POLARS - Manipulación de Datos con Python

Por Jose R. Zapata

Ultima actualizacion: 7/Agosto/2024 - Polars 1.4.1

Invítame a un Café

Polars es una biblioteca de manipulación de datos de alto rendimiento desarrollada por Ritchie Vink. Inicialmente, Polars fue diseñado para ofrecer una alternativa más rápida y eficiente a pandas, utilizando técnicas modernas de procesamiento paralelo y optimización de memoria. Polars está escrito en Rust, lo que le permite manejar grandes volúmenes de datos de manera eficiente y segura, aprovechando al máximo los recursos del hardware disponible.

Los principales tipos de datos que pueden representarse con Polars son:

  • Datos tabulares con columnas de tipo heterogéneo con etiquetas en columnas y filas.
  • Series temporales.

Polars proporciona herramientas que permiten:

  • Leer y escribir datos en diferentes formatos: CSV, JSON, Parquet y más.
  • Seleccionar y filtrar tablas de datos de manera sencilla en función de posición, valor o etiquetas.
  • Fusionar y unir datos.
  • Transformar datos aplicando funciones tanto globalmente como por ventanas.
  • Manipulación de series temporales.
  • Hacer gráficas.

En Polars existen 2 tipos básicos de objetos:

  • Series (listas, 1D)
  • DataFrame (tablas, 2D)

Polars nos proporciona las estructuras de datos y funciones necesarias para el análisis de datos de una manera eficiente y rápida, aprovechando las ventajas del procesamiento paralelo y la optimización de memoria. Esto hace que Polars sea una excelente opción para proyectos que requieren manejar grandes volúmenes de datos y realizar análisis complejos de manera eficiente.

la filosofia de polars es en general:

  • Utilice todos los núcleos del procesador disponibles en tu máquina.
  • Optimizar las consultas para reducir el trabajo innecesario y las asignaciones de memoria.
  • Maneje datos mucho más grandes que la RAM disponible en la maquina.

Si ya sabes usar pandas lo recomendable es que entiendas las diferencias entre Polars y Pandas:

Migracion desde pandas: https://docs.pola.rs/user-guide/migration/pandas/

Instalar Polars

PIP

pip install polars

Polars con librerias para manipular dataframes de pandas

pip install 'polars[pandas]'

Pandas para leer archivos de excel

pip install 'polars[xlsx2csv]'

Para ver otras opciones ir al siguiente link otras instalaciones de polars

Recomiendo realizar la instalacion de polars con compatibilidad para pandas, pyarrow, xlsx2csv

pip install 'polars[pandas,pyarrow,xlsx2csv]'

En google colab ya esta instalada polars con la opciones de pandas y pyarrow, pero no esta la ultima version entonces para instalar la ultima version de polars

#!pip install --upgrade 'polars[pandas,pyarrow,xlsx2csv]'

Importando Polars

La libreria Polars se importa de la siguiente manera

import polars as pl # Importacion estandar de la libreria Polars
import numpy as np # Importacion estandar de la libreria NumPy
pl.__version__
'1.4.1'

Series

Una serie es el primer tipo de datos de Polars y es similar a una matriz NumPy. Lo que diferencia una matriz NumPy de una serie en Polars es que una serie puede tener etiquetas en los ejes, lo que significa que puede ser indexada por una etiqueta, en lugar de solo una ubicación numérica. Además, no necesita contener datos numéricos; puede contener cualquier objeto arbitrario compatible con Polars. Las series en Polars están diseñadas para ser eficientes y rápidas, aprovechando el rendimiento de Rust para manejar grandes volúmenes de datos de manera eficiente.

Creando una Serie

Puede convertir una lista, una matriz numpy o un diccionario en una serie, usando el método pl.Series:

# Crear diferentes tipos de datos
labels = ['a','b','c'] # lista de etiquetas
my_list = [10,20,30] # lista con valores
arr = np.array([10,20,30]) # Convertir ista de valores en arreglo NumPy

Desde Listas

# Convertir una lista en series usando el método pl.Series
# observe que se crean los nombres con las posiciones de cada elemento
pl.Series(values=my_list)
shape: (3,)
i64
10
20
30
# Convertir una lista en series usando el método pl.Series
# se puede ingresar el nombre de la serie
pl.Series(name= "numeros" ,values=my_list)
shape: (3,)
numeros
i64
10
20
30
# No es necesario ingresar la palabra de 'values =''  en el argumento
# Convertir una lista en series usando el método pl.Series
# se puede ingresar el nombre de la serie
pl.Series(my_list)
shape: (3,)
i64
10
20
30

Desde Arreglos NumPy

# Convertir un arreglo en series usando el método pl.Series
pl.Series(arr)
shape: (3,)
i64
10
20
30
# Convertir un arreglo en series indicando también el nombre
pl.Series("numpy",arr)
shape: (3,)
numpy
i64
10
20
30

Datos en una Series

Una serie de polars puede contener una variedad de tipos de objetos:

# Creando una serie basado solo en una lista de letras
pl.Series(values=labels)
shape: (3,)
str
"a"
"b"
"c"

Indexacion

Es diferente que pandas, Polars no utiliza un índice y cada fila se indexa por su posición entera en la tabla.

Veamos algunos ejemplos de cómo obtener información de una serie. Vamos a crear dos series, ser1 y ser2:

# Creacion de una serie con sus labels o indices
ser1 = pl.Series([1,2,3,4])
ser1
shape: (4,)
i64
1
2
3
4
# Creacion de una serie con sus labels o indices
ser2 = pl.Series([1,2,5,4])
ser2
shape: (4,)
i64
1
2
5
4
# La busqueda en una serie es igual como en una lista, usando el numero de su posicion
ser1[0]
1
ser2[3]
4

Las operaciones también se realizan según el índice:

# SOn operaciones 1 a 1
ser1 + ser2
shape: (4,)
i64
2
4
8
8

DataFrames

Los DataFrames son la estructura mas importante en polars y están directamente inspirados en el lenguaje de programación R. Se puede pensar en un DataFrame como un conjunto de Series reunidas. En los DataFrame tenemos la opción de especificar tanto el columns (el nombre de las columnas).

# Importar la función de NumPy para crear arreglos de números enteros
from numpy.random import randn
np.random.seed(101) # Inicializar el generador aleatorio
# Crear un dataframe con números aleatorios de 4 Columnas y 5 Filas
# Esto evita tener que escribir repetidamente las comas

df = pl.DataFrame(
    {
        "W": randn(6),
        "X": randn(6),
        "Y": randn(6),
        "Z": randn(6)
    }
)
df
shape: (6, 4)
WXYZ
f64f64f64f64
2.70685-0.8480770.1886952.605967
0.6281330.605965-0.7588720.683509
0.907969-2.018168-0.9332370.302665
0.5038260.7401220.9550571.693723
0.6511180.5288130.190794-1.706086
-0.319318-0.5890011.978757-1.159119

Descripcion general del dataframe

Numero de Filas y Columnas

df.shape # retorna un Tuple asi: (filas, col)
(6, 4)

información General de los datos

df.collect_schema()
Schema([('W', Float64), ('X', Float64), ('Y', Float64), ('Z', Float64)])
# solo ver los Tipos de datos que existen en las columnas del dataframe
df.dtypes
[Float64, Float64, Float64, Float64]

Resumen de estadistica descriptiva General

el método .describe() de los dataframes presenta un resumen de la estadistica descriptiva general de las columnas numericas del dataframe, presenta la información de:

  • Promedio (mean)
  • Desviacion estandard (std)
  • Valor minimo
  • Valor maximo
  • Cuartiles (25%, 50% y 75%)
df.describe() # No muestra la información de las columnas categoricas
shape: (9, 5)
statisticWXYZ
strf64f64f64f64
"count"6.06.06.06.0
"null_count"0.00.00.00.0
"mean"0.84643-0.2633910.2701990.403443
"std"1.0027061.0878911.0870511.642939
"min"-0.319318-2.018168-0.933237-1.706086
"25%"0.503826-0.848077-0.758872-1.159119
"50%"0.6511180.5288130.1907940.683509
"75%"0.9079690.6059650.9550571.693723
"max"2.706850.7401221.9787572.605967

Ver Primeros elementos del dataframe

Sino se agerga un numero el default sera 5

df.head(3)
shape: (3, 4)
WXYZ
f64f64f64f64
2.70685-0.8480770.1886952.605967
0.6281330.605965-0.7588720.683509
0.907969-2.018168-0.9332370.302665

Ver Ultimos elementos del dataframe

Sino se agerga un numero el default sera 5

df.tail(2)
shape: (2, 4)
WXYZ
f64f64f64f64
0.6511180.5288130.190794-1.706086
-0.319318-0.5890011.978757-1.159119

Ver elementos aleatorios del dataframe

df.sample(3)
shape: (3, 4)
WXYZ
f64f64f64f64
2.70685-0.8480770.1886952.605967
-0.319318-0.5890011.978757-1.159119
0.6281330.605965-0.7588720.683509

Seleccion y Indexacion

Existen diversos métodos para tomar datos de un DataFrame

# Regresara todos los datos de la columna W
df['W']
shape: (6,)
W
f64
2.70685
0.628133
0.907969
0.503826
0.651118
-0.319318
# Seleccionar dos o mas columnas
# Pasar una lista con los nombres de las columnas

df[['W','Z']]
shape: (6, 2)
WZ
f64f64
2.706852.605967
0.6281330.683509
0.9079690.302665
0.5038261.693723
0.651118-1.706086
-0.319318-1.159119
# Seleccionar dos o mas columnas
# Pasar una lista con los nombres de las columnas
# Puedo indicar el orden de las columnas

df[['X','W','Z']]
shape: (6, 3)
XWZ
f64f64f64
-0.8480772.706852.605967
0.6059650.6281330.683509
-2.0181680.9079690.302665
0.7401220.5038261.693723
0.5288130.651118-1.706086
-0.589001-0.319318-1.159119

Las columnas de un DataFrame Columns son solo Series

type(df['W']) # Tipos de datos
polars.series.series.Series

Creando una Nueva Columna

Se realiza con el metodo .with_columns() y el nombre de la nueva columna se realiza con el metodo .alias()

# Nueva columna igual a la suma de otras dos
# operación vectorizada
df = df.with_columns((df["W"] + df["Y"]).alias("new"))
df
shape: (6, 5)
WXYZnew
f64f64f64f64f64
2.70685-0.8480770.1886952.6059672.895545
0.6281330.605965-0.7588720.683509-0.130739
0.907969-2.018168-0.9332370.302665-0.025268
0.5038260.7401220.9550571.6937231.458882
0.6511180.5288130.190794-1.7060860.841912
-0.319318-0.5890011.978757-1.1591191.659439

Eliminando Columnas

df.drop('new')
shape: (6, 4)
WXYZ
f64f64f64f64
2.70685-0.8480770.1886952.605967
0.6281330.605965-0.7588720.683509
0.907969-2.018168-0.9332370.302665
0.5038260.7401220.9550571.693723
0.6511180.5288130.190794-1.706086
-0.319318-0.5890011.978757-1.159119
# No se aplica a el dataframe a menos que se especifique.
# Como se ve la operación pasada no quedo grabada
df
shape: (6, 5)
WXYZnew
f64f64f64f64f64
2.70685-0.8480770.1886952.6059672.895545
0.6281330.605965-0.7588720.683509-0.130739
0.907969-2.018168-0.9332370.302665-0.025268
0.5038260.7401220.9550571.6937231.458882
0.6511180.5288130.190794-1.7060860.841912
-0.319318-0.5890011.978757-1.1591191.659439
# Para que quede grabado se debe grabar el resultado

df = df.drop('new') # Elimina la columna new
df
shape: (6, 4)
WXYZ
f64f64f64f64
2.70685-0.8480770.1886952.605967
0.6281330.605965-0.7588720.683509
0.907969-2.018168-0.9332370.302665
0.5038260.7401220.9550571.693723
0.6511180.5288130.190794-1.706086
-0.319318-0.5890011.978757-1.159119
df.columns # nombres de las columnas
['W', 'X', 'Y', 'Z']

Obtener los nombres de las columnas y los indices (index):

Seleccionando Filas y Columnas

las dos formas de seleccion principal son:

  1. Usando []

    • DataFrame[etiqueta_columna] <- por etiquetas de las columnas
    • DataFrame[indice_fila, indice_columna] <- por indices
  2. Usando expresiones como filter, select and with_columns

    • Usa filter cuando necesites filtrar filas basadas en condiciones.
    • Usa select cuando necesites seleccionar y/o transformar columnas.
    • Usa with_columns cuando necesites añadir o modificar una o más columnas en el DataFrame. Puedes crear nuevas columnas o actualizar las existentes basándote en expresiones.
df["Z"] # se selecciona todos los valores de la Columna 'Z'
shape: (6,)
Z
f64
2.605967
0.683509
0.302665
1.693723
-1.706086
-1.159119

O basado en la posición (index) en vez de usar la etiqueta

df[:,2] # Se seleccionan todos los valores de la Columna con indice 2
# recordar que los index empiezan en cero
shape: (6,)
Y
f64
0.188695
-0.758872
-0.933237
0.955057
0.190794
1.978757

Seleccionar un subconjunto de filas y columnas

# Mediante etiquetas
# se selecciona el elemento que esta en la fila=1 Col=Y
df[1,'Y'] # con etiquetas
-0.758872056210466
# Mediante posicion y etiquetas
# se selecciona un subconjunto de datos que estan entre
# filas = 0, 1   Cols= W, Y
df[[0,1],['W','Y']]
shape: (2, 2)
WY
f64f64
2.706850.188695
0.628133-0.758872
df[[1,0],['Y','W']]
shape: (2, 2)
YW
f64f64
-0.7588720.628133
0.1886952.70685

Seleccion Condicional o Filtros

Una característica importante de polars es la selección condicional usando el metodo .filter():

df
shape: (6, 4)
WXYZ
f64f64f64f64
2.70685-0.8480770.1886952.605967
0.6281330.605965-0.7588720.683509
0.907969-2.018168-0.9332370.302665
0.5038260.7401220.9550571.693723
0.6511180.5288130.190794-1.706086
-0.319318-0.5890011.978757-1.159119

Los terminos de busqueda condicional o filtros se entregan al método como tipo ‘string’

# seleccionar todas las filas donde el valor
# que esta en la columna 'W' sea mayor que cero

df.filter(pl.col('W') > 0) # pl.col('W') > 0 crea la condición booleana para la columna W.
shape: (5, 4)
WXYZ
f64f64f64f64
2.70685-0.8480770.1886952.605967
0.6281330.605965-0.7588720.683509
0.907969-2.018168-0.9332370.302665
0.5038260.7401220.9550571.693723
0.6511180.5288130.190794-1.706086

Para dos condiciones, se usa los booleanos de esta forma

  • | en vez de or
  • & en vez de and
  • ~ en vez de not

Recuerde usar paréntesis:

# Seleccionar las filas donde 'W' sea mayor que cero
# y también donde 'Y' sea mayor que 0.5

df.filter((pl.col('W') > 0) & (pl.col('Y') > 0.5))
shape: (1, 4)
WXYZ
f64f64f64f64
0.5038260.7401220.9550571.693723
# Seleccionar las filas donde 'W' sea mayor que cero
# y de esas filas escoger los valores de la columna 'Y'

df.filter((pl.col('W') > 0))['Y']
shape: (5,)
Y
f64
0.188695
-0.758872
-0.933237
0.955057
0.190794
# Seleccionar las filas donde 'W' sea mayor que cero
# y de esas filas escoger los valores de las columna 'Y' y 'Z'

df.filter((pl.col('W') > 0))[['Y','Z']]
shape: (5, 2)
YZ
f64f64
0.1886952.605967
-0.7588720.683509
-0.9332370.302665
0.9550571.693723
0.190794-1.706086

Para dos condiciones, puede usar | = or y & = and con paréntesis:

# Seleccionar las filas donde 'W' sea mayor que cero
# y también donde 'Y' sea mayor que 0.5

df.filter((pl.col('W') > 0) & (pl.col('Y') > 0.5))
shape: (1, 4)
WXYZ
f64f64f64f64
0.5038260.7401220.9550571.693723

Crear una columna de Indexacion

En Polars, a diferencia de Pandas, no existe el concepto de un índice nombrado para las filas, ya que las filas están indexadas de manera implícita con números enteros comenzando desde 0. Polars no tiene un índice explícito como Pandas, pero puedes lograr un efecto similar usando una columna adicional como índice.

Esta columna no tendra ninguna propiedad especial.

df
shape: (6, 4)
WXYZ
f64f64f64f64
2.70685-0.8480770.1886952.605967
0.6281330.605965-0.7588720.683509
0.907969-2.018168-0.9332370.302665
0.5038260.7401220.9550571.693723
0.6511180.5288130.190794-1.706086
-0.319318-0.5890011.978757-1.159119
# Crear columna indice con valores por defecto desde 0,1...n index
df = df.with_row_index()
df
shape: (6, 5)
indexWXYZ
u32f64f64f64f64
02.70685-0.8480770.1886952.605967
10.6281330.605965-0.7588720.683509
20.907969-2.018168-0.9332370.302665
30.5038260.7401220.9550571.693723
40.6511180.5288130.190794-1.706086
5-0.319318-0.5890011.978757-1.159119

Groupby (Agrupacion por filas)

El método groupby le permite agrupar filas de datos y llamar a funciones agregadas

import polars as pl
# Crear dataframe desde un diccionario
data = {'Company':['GOOG','GOOG','MSFT','MSFT','FB','FB','GOOG','MSFT','FB'],
       'Person':['Sam','Charlie','Amy','Vanessa','Carl','Sarah','John','Randy','David'],
       'Sales':[200,120,340,124,243,350,275,400,180]}
data
{'Company': ['GOOG', 'GOOG', 'MSFT', 'MSFT', 'FB', 'FB', 'GOOG', 'MSFT', 'FB'],
 'Person': ['Sam',
  'Charlie',
  'Amy',
  'Vanessa',
  'Carl',
  'Sarah',
  'John',
  'Randy',
  'David'],
 'Sales': [200, 120, 340, 124, 243, 350, 275, 400, 180]}
#conversion del diccionario a dataframe
df = pl.DataFrame(data)
df
shape: (9, 3)
CompanyPersonSales
strstri64
"GOOG""Sam"200
"GOOG""Charlie"120
"MSFT""Amy"340
"MSFT""Vanessa"124
"FB""Carl"243
"FB""Sarah"350
"GOOG""John"275
"MSFT""Randy"400
"FB""David"180

Se puede usar el método .groupby() para agrupar filas en función de un nombre de columna. Por ejemplo, vamos a agruparnos a partir de la Compañía. Esto creará un objeto polars.dataframe.group_by.GroupBy:

#agrupar por Company
df.group_by('Company')
<polars.dataframe.group_by.GroupBy at 0x723f6e15f8b0>

Se puede grabar este objeto en una nueva variable:

by_comp = df.group_by("Company")

utilizar los métodos agregados del objeto:

# Promedio de todas las columnas por company
by_comp.mean()
shape: (3, 3)
CompanyPersonSales
strstrf64
"FB"null257.666667
"MSFT"null288.0
"GOOG"null198.333333
# Promedio de ventas por company, es bueno indicar las columnas numericas
# Para que tenga sentido
by_comp.mean()[["Company","Sales"]]
shape: (3, 2)
CompanySales
strf64
"MSFT"288.0
"FB"257.666667
"GOOG"198.333333
# agrupar por compañia y calcular el promedio por cada una
df.group_by('Company').mean()[["Company","Sales"]]
shape: (3, 2)
CompanySales
strf64
"FB"257.666667
"GOOG"198.333333
"MSFT"288.0

Más ejemplos de métodos agregados:

# agrupar por compañia y calcular la desviacion estandard
by_comp.agg(pl.col('Sales').std())
shape: (3, 2)
CompanySales
strf64
"GOOG"77.51344
"FB"85.94378
"MSFT"145.161978
# agrupar por compañia y calcular el minimo
by_comp.min()[["Company","Sales"]]
shape: (3, 2)
CompanySales
stri64
"FB"180
"MSFT"124
"GOOG"120
# agrupar por compañia y calcular el maximo
by_comp.max()[["Company","Sales"]]
shape: (3, 2)
CompanySales
stri64
"GOOG"275
"MSFT"400
"FB"350
# agrupar por compañia y sumar los elementos que hay excluyendo los NaN
by_comp.len()
shape: (3, 2)
Companylen
stru32
"MSFT"3
"FB"3
"GOOG"3
# Para Generar estadísticas descriptivas que resuman la tendencia central,
# la dispersión y la forma de la distribución de un conjunto de datos,
# se pueden agrupar los calculos ccon el metodo .agg

by_comp.agg([
    pl.col("Sales").mean().alias("mean"),
    pl.col("Sales").std().alias("std"),
    pl.col("Sales").min().alias("min"),
    pl.col("Sales").max().alias("max"),
    pl.col("Sales").median().alias("median"),
    pl.col("Sales").count().alias("count")
])
shape: (3, 7)
Companymeanstdminmaxmediancount
strf64f64i64i64f64u32
"FB"257.66666785.94378180350243.03
"MSFT"288.0145.161978124400340.03
"GOOG"198.33333377.51344120275200.03

Pivot Tables

La funciónlidad “Pivot_table” es muy utilizada y popular en las conocidas “hojas de cálculo” tipo, OpenOffice, LibreOffice, Excel, Lotus, etc. Esta funcionalidad nos permite agrupar, ordenar, calcular datos y manejar datos de una forma muy similar a la que se hace con las hojas de cálculo. mas informacion

La principal función del .pivot son las agrupaciones de datos a las que se les suelen aplicar funciones matemáticas como sumatorios, promedios, etc

import seaborn as sns # importar la librería seaborn
# cargar dataset del titanic
titanic = sns.load_dataset('titanic')
titanic.head()

survivedpclasssexagesibspparchfareembarkedclasswhoadult_maledeckembark_townalivealone
003male22.0107.2500SThirdmanTrueNaNSouthamptonnoFalse
111female38.01071.2833CFirstwomanFalseCCherbourgyesFalse
213female26.0007.9250SThirdwomanFalseNaNSouthamptonyesTrue
311female35.01053.1000SFirstwomanFalseCSouthamptonyesFalse
403male35.0008.0500SThirdmanTrueNaNSouthamptonnoTrue
type(titanic)
pandas.core.frame.DataFrame
# convertir dataframe de pandas a polars
titanic = pl.from_pandas(titanic)
type(titanic)
polars.dataframe.frame.DataFrame

Haciendo el Pivot table a mano para obtener el promedio de personas que sobrevivieron por genero

# 1. Agrupar por genero
# 2. Obtener los sobrevivientes
# 3. Calcular el promedio
titanic.group_by('sex').mean()[["sex", "survived"]]
shape: (2, 2)
sexsurvived
strf64
"male"0.188908
"female"0.742038

promedio de cuantos sobrevivieron por genero divididos por clase

# 1. Agrupar por genero y clase
# 2. Calcular el promedio
# 3. Obtener los sobrevivientes
titanic.group_by(['sex', 'class']).mean()['sex', 'class','survived']
shape: (6, 3)
sexclasssurvived
strcatf64
"female""Third"0.5
"female""First"0.968085
"male""Third"0.135447
"male""First"0.368852
"male""Second"0.157407
"female""Second"0.921053

Usando Pivot tables

# Agrupar por 'sex' y 'class', calcular la media de 'survived' y hacer un pivot
grouped = titanic.group_by(['sex', 'class']).agg(
    pl.col('survived').mean()
)

# Crear una tabla dinámica (pivot table)
pivot_table = grouped.pivot(
    values='survived',
    index='sex',
    on='class'
)

pivot_table
shape: (2, 4)
sexFirstThirdSecond
strf64f64f64
"male"0.3688520.1354470.157407
"female"0.9680850.50.921053

Concatenar y Unir (Concatenating, Joining)

Hay 2 formas principales de combinar DataFrames: concatenar y unir.

# DataFrames de ejemplo para concatenación
import polars as pl
df1 = pl.DataFrame({'A': ['A0', 'A1', 'A2', 'A3'],
                        'B': ['B0', 'B1', 'B2', 'B3'],
                        'C': ['C0', 'C1', 'C2', 'C3'],
                        'D': ['D0', 'D1', 'D2', 'D3']})
df2 = pl.DataFrame({'A': ['A4', 'A5', 'A6', 'A7'],
                        'B': ['B4', 'B5', 'B6', 'B7'],
                        'C': ['C4', 'C5', 'C6', 'C7'],
                        'D': ['D4', 'D5', 'D6', 'D7']})
df3 = pl.DataFrame({'A': ['A8', 'A9', 'A10', 'A11'],
                        'B': ['B8', 'B9', 'B10', 'B11'],
                        'C': ['C8', 'C9', 'C10', 'C11'],
                        'D': ['D8', 'D9', 'D10', 'D11']})
df1
shape: (4, 4)
ABCD
strstrstrstr
"A0""B0""C0""D0"
"A1""B1""C1""D1"
"A2""B2""C2""D2"
"A3""B3""C3""D3"
df2
shape: (4, 4)
ABCD
strstrstrstr
"A4""B4""C4""D4"
"A5""B5""C5""D5"
"A6""B6""C6""D6"
"A7""B7""C7""D7"
df3
shape: (4, 4)
ABCD
strstrstrstr
"A8""B8""C8""D8"
"A9""B9""C9""D9"
"A10""B10""C10""D10"
"A11""B11""C11""D11"

Concatenación (Concatenation)

La concatenación básicamente combina DataFrames. Tenga en cuenta que las dimensiones deben coincidir a lo largo del eje con el que se está concatenando.

la Concatenación se hace con dataframes de diferentes indices

Puede usar .concat() y pasar una lista de DataFrames para concatenar juntos:

# Concatenar cada dataframe verticalmente,
# ya que coinciden los nombres de las columnas

pl.concat([df1,df2,df3], how="vertical")
shape: (12, 4)
ABCD
strstrstrstr
"A0""B0""C0""D0"
"A1""B1""C1""D1"
"A2""B2""C2""D2"
"A3""B3""C3""D3"
"A4""B4""C4""D4"
"A7""B7""C7""D7"
"A8""B8""C8""D8"
"A9""B9""C9""D9"
"A10""B10""C10""D10"
"A11""B11""C11""D11"

Los dataframes anteriores No se pueden concatenar Horizontalmente por que los dataframes tienen columnas con el mismo nombre

df_h1 = pl.DataFrame({"l1": [1, 2], "l2": [3, 4]})
df_h2 = pl.DataFrame({"r1": [5, 6], "r2": [7, 8], "r3": [9, 10]})
pl.concat([df_h1, df_h2], how="horizontal")
shape: (2, 5)
l1l2r1r2r3
i64i64i64i64i64
13579
246810

Fusion (Join)

El metodo join() le permite fusionar DataFrames juntos utilizando una lógica similar a la combinación de Tablas SQL. Por ejemplo:

# DataFrames de ejemplo para merging
left = pl.DataFrame({
    'Producto': ['Arepas', 'Banano', 'Cafe'],
    'Tienda': [1, 2, 1],
    'Ventas': [100, 200, 50]
})

right = pl.DataFrame({
    'Producto': ['Arepas', 'Banano', 'Cafe'],
    'Tienda': [1, 2, 1],
    'Inventario': [50, 100, 75]
})
left
shape: (3, 3)
ProductoTiendaVentas
stri64i64
"Arepas"1100
"Banano"2200
"Cafe"150
right
shape: (3, 3)
ProductoTiendaInventario
stri64i64
"Arepas"150
"Banano"2100
"Cafe"175
# how='inner' utilice la intersección de las claves de ambos marcos, similar a una combinación interna de SQL;
# las keys son comunes

left.join(right, on='Producto', how='inner')
shape: (3, 5)
ProductoTiendaVentasTienda_rightInventario
stri64i64i64i64
"Arepas"1100150
"Banano"22002100
"Cafe"150175

Un ejemplo mas complicado:

  • Natural join: para mantener solo las filas que coinciden con los marcos de datos, especifique el argumento how = ‘inner’.
  • Full outer join: para mantener todas las filas de ambos dataframe, especifique how = ‘full’ antes se llamaba outer.
  • Left outer join: para incluir todas las filas de su dataframe x y solo aquellas de y que coincidan, especifique how=‘left’.
  • Right outer join: para incluir todas las filas de su dataframe y y solo aquellas de x que coincidan, especifique how=‘right’.

left = pl.DataFrame({
    'Producto': ['Arepas', 'Leche', 'Cafe'],
    'Tienda': [1, 2, 1],
    'Ventas': [100, 200, 50]
})

right = pl.DataFrame({
    'Producto': ['Arepas', 'Banano', 'Cafe'],
    'Tienda': [1, 2, 1],
    'Inventario': [50, 100, 75]
})
left
shape: (3, 3)
ProductoTiendaVentas
stri64i64
"Arepas"1100
"Leche"2200
"Cafe"150
right
shape: (3, 3)
ProductoTiendaInventario
stri64i64
"Arepas"150
"Banano"2100
"Cafe"175
# fusionando comparando las mismas claves que tengan comunes
left.join(right, on='Producto')
shape: (2, 5)
ProductoTiendaVentasTienda_rightInventario
stri64i64i64i64
"Arepas"1100150
"Cafe"150175
# fusionando totalmente las dos tablas con las claves
left.join(right, on='Producto', how='full')
shape: (4, 6)
ProductoTiendaVentasProducto_rightTienda_rightInventario
stri64i64stri64i64
"Arepas"1100"Arepas"150
nullnullnull"Banano"2100
"Cafe"150"Cafe"175
"Leche"2200nullnullnull
# fusionando usando las claves de la tabla right
left.join(right, on='Producto', how='right')
shape: (3, 5)
TiendaVentasProductoTienda_rightInventario
i64i64stri64i64
1100"Arepas"150
nullnull"Banano"2100
150"Cafe"175
# fusionando usando las claves de la tabla left
left.join(right, on='Producto', how='left')
shape: (3, 5)
ProductoTiendaVentasTienda_rightInventario
stri64i64i64i64
"Arepas"1100150
"Leche"2200nullnull
"Cafe"150175

Datos Categoricos

  • La busqueda en datos categoricos es mucho mas rapida
  • Ocupan Menos memoria que si los datos estan como string
  • Se pueden tener datos categoricos Ordinales

Mas información de datos categoricos:

https://docs.pola.rs/user-guide/concepts/data-types/categoricals/

Creacion

# Creacion de una serie de datos categoricos
cate = pl.Series(["manzana", "banano", "corozo", "manzana","pera"],
                 dtype=pl.Categorical)
cate
shape: (5,)
cat
"manzana"
"banano"
"corozo"
"manzana"
"pera"
cate.dtype
Categorical(ordering='physical')
cate.describe()
shape: (2, 2)
statisticvalue
strstr
"count""5"
"null_count""0"
# Creando primero los datos y luego convirtiendolos en categoricos
df_cate = pl.DataFrame({"Fruta":["manzana", "banano", "corozo", "manzana","pera"]})
df_cate
shape: (5, 1)
Fruta
str
"manzana"
"banano"
"corozo"
"manzana"
"pera"
# Observar los tipos de datos en el data frame
df_cate.dtypes
[String]
df_cate = df_cate.with_columns(
    pl.col("Fruta").cast(pl.Categorical).alias("Fruta2")
)
df_cate
shape: (5, 2)
FrutaFruta2
strcat
"manzana""manzana"
"banano""banano"
"corozo""corozo"
"manzana""manzana"
"pera""pera"
# Observar los tipos de datos en el dataframe
df_cate.dtypes
[String, Categorical(ordering='physical')]
# Schema de los datos
df_cate.schema
Schema([('Fruta', String), ('Fruta2', Categorical(ordering='physical'))])
# Crear los datos categoricos desde a declaracion de los datos
df_cate = pl.DataFrame({
    'A': pl.Series(list('abca')).cast(pl.Categorical),
    'B': pl.Series(list('bccd')).cast(pl.Categorical)
})
df_cate
shape: (4, 2)
AB
catcat
"a""b"
"b""c"
"c""c"
"a""d"
df_cate.dtypes
[Categorical(ordering='physical'), Categorical(ordering='physical')]
df_cate.describe()
shape: (9, 3)
statisticAB
strstrstr
"count""4""4"
"null_count""0""0"
"mean"nullnull
"std"nullnull
"min"nullnull
"25%"nullnull
"50%"nullnull
"75%"nullnull
"max"nullnull

Categoricos Ordinales

Se utiliza el tipo de dato Enum

https://docs.pola.rs/user-guide/concepts/data-types/categoricals/

# Crear los datos categoricos desde a declaracion de los datos
df_cate = pl.DataFrame({
    'A': pl.Series(list('abca')).cast(pl.Categorical),
    'B': pl.Series(list('bccd')).cast(pl.Categorical)
})
df_cate
shape: (4, 2)
AB
catcat
"a""b"
"b""c"
"c""c"
"a""d"
df_cate.dtypes
[Categorical(ordering='physical'), Categorical(ordering='physical')]
# definición de los tipos de datos y que estan en orden
my_order = ["a", "b", "c", "d"]

df_cate = df_cate.with_columns(
    [pl.col("A").cast(pl.Enum(my_order))],
)
df_cate
shape: (4, 2)
AB
enumcat
"a""b"
"b""c"
"c""c"
"a""d"
df_cate.dtypes
[Enum(categories=['a', 'b', 'c', 'd']), Categorical(ordering='physical')]
# definición de los tipos de datos y con orden propio
my_order = ["d", "b", "a", "c"]

df_cate = df_cate.with_columns(
    [pl.col("B").cast(pl.Enum(my_order))],
)
df_cate
shape: (4, 2)
AB
enumenum
"a""b"
"b""c"
"c""c"
"a""d"
df_cate.dtypes
[Enum(categories=['a', 'b', 'c', 'd']), Enum(categories=['d', 'b', 'a', 'c'])]

Correcion de tipos de datos (Casting)

Cuando se trabaja con datos, es posible que se encuentre con un DataFrame donde el tipo de datos de una columna no es la correcta. los tipos de datos son principalmente: numericos, categoricos nominales, categoricos ordinales, booleanos, fechas y tiempos, texto.

Se debe convertir los tipos de datos para que sean apropiados para el tipo de datos que representan. Por ejemplo, es posible que desee convertir una columna de strings que representan números en números enteros o flotantes. O puede tener una columna de números que representan fechas, pero que polars no reconoce como fechas.

Nota: al convertir los datos a su forma correcta, normalmente hace que los DataFrames sean mucho más pequeños en la memoria, lo que los hace más eficientes de usar.

import seaborn as sns # importar la libreria seaborn
titanic = sns.load_dataset('titanic')

# Convertir dataframe pandas a Polars
titanic = pl.from_pandas(titanic)
titanic.sample(5)
shape: (5, 15)
survivedpclasssexagesibspparchfareembarkedclasswhoadult_maledeckembark_townalivealone
i64i64strf64i64i64f64strcatstrboolcatstrstrbool
02"male"30.00013.0"S""Second""man"truenull"Southampton""no"true
13"male"39.0007.925"S""Third""man"truenull"Southampton""yes"true
12"female"2.01126.0"S""Second""child"falsenull"Southampton""yes"false
03"male"null007.8958"S""Third""man"truenull"Southampton""no"true
03"male"null007.8958"C""Third""man"truenull"Cherbourg""no"true

Casting variables Categoricas nominales

# convertir la columna sex, embarked, who, embark_town en categoricas
cols_categoricas_nom = ["sex", "embarked", "who", "embark_town", "alive"]

# ver los datos unicos de las columnas_categoricas_nom
for columna in cols_categoricas_nom:
  print(f"Valores únicos en {columna}: {titanic[columna].unique()}")
Valores únicos en sex: shape: (2,)
Series: 'sex' [str]
[
	"female"
	"male"
]
Valores únicos en embarked: shape: (4,)
Series: 'embarked' [str]
[
	null
	"Q"
	"S"
	"C"
]
Valores únicos en who: shape: (3,)
Series: 'who' [str]
[
	"child"
	"woman"
	"man"
]
Valores únicos en embark_town: shape: (4,)
Series: 'embark_town' [str]
[
	null
	"Southampton"
	"Cherbourg"
	"Queenstown"
]
Valores únicos en alive: shape: (2,)
Series: 'alive' [str]
[
	"yes"
	"no"
]
titanic = titanic.with_columns([
    pl.col(col).cast(pl.Categorical) for col in cols_categoricas_nom
])

# ver los datos unicos de las columnas_categoricas_nom
for columna in cols_categoricas_nom:
  print(f"Valores únicos en {columna}: {titanic[columna].unique()}")
Valores únicos en sex: shape: (2,)
Series: 'sex' [cat]
[
	"male"
	"female"
]
Valores únicos en embarked: shape: (4,)
Series: 'embarked' [cat]
[
	null
	"S"
	"C"
	"Q"
]
Valores únicos en who: shape: (3,)
Series: 'who' [cat]
[
	"man"
	"woman"
	"child"
]
Valores únicos en embark_town: shape: (4,)
Series: 'embark_town' [cat]
[
	null
	"Southampton"
	"Cherbourg"
	"Queenstown"
]
Valores únicos en alive: shape: (2,)
Series: 'alive' [cat]
[
	"no"
	"yes"
]
titanic.schema
Schema([('survived', Int64),
        ('pclass', Int64),
        ('sex', Categorical(ordering='physical')),
        ('age', Float64),
        ('sibsp', Int64),
        ('parch', Int64),
        ('fare', Float64),
        ('embarked', Categorical(ordering='physical')),
        ('class', Categorical(ordering='physical')),
        ('who', Categorical(ordering='physical')),
        ('adult_male', Boolean),
        ('deck', Categorical(ordering='physical')),
        ('embark_town', Categorical(ordering='physical')),
        ('alive', Categorical(ordering='physical')),
        ('alone', Boolean)])

Casting variables Categoricas Ordinales

cols_categoricas_ord = ["pclass", "deck"]

for columna in cols_categoricas_ord:
  print(f"Valores únicos en {columna}: {titanic[columna].unique()}")
Valores únicos en pclass: shape: (3,)
Series: 'pclass' [i64]
[
	1
	2
	3
]
Valores únicos en deck: shape: (8,)
Series: 'deck' [cat]
[
	null
	"A"
	"B"
	"C"
	"D"
	"E"
	"F"
	"G"
]
# Convertir la columna 'pclass' en categórica con un orden específico
titanic = titanic.with_columns(
    pl.col("pclass").cast(pl.Utf8).cast(pl.Enum(['3', '2', '1']))
)
# Convertir la columna 'deck' en categórica con un orden específico
titanic = titanic.with_columns(
    pl.col("deck").cast(pl.Enum(list("ABCDEFG")))
)
titanic.schema
Schema([('survived', Int64),
        ('pclass', Enum(categories=['3', '2', '1'])),
        ('sex', Categorical(ordering='physical')),
        ('age', Float64),
        ('sibsp', Int64),
        ('parch', Int64),
        ('fare', Float64),
        ('embarked', Categorical(ordering='physical')),
        ('class', Categorical(ordering='physical')),
        ('who', Categorical(ordering='physical')),
        ('adult_male', Boolean),
        ('deck', Enum(categories=['A', 'B', 'C', 'D', 'E', 'F', 'G'])),
        ('embark_town', Categorical(ordering='physical')),
        ('alive', Categorical(ordering='physical')),
        ('alone', Boolean)])
for columna in cols_categoricas_ord:
  print(f"Valores únicos en {columna}: {titanic[columna].unique()}")
Valores únicos en pclass: shape: (3,)
Series: 'pclass' [enum]
[
	"3"
	"2"
	"1"
]
Valores únicos en deck: shape: (8,)
Series: 'deck' [enum]
[
	null
	"A"
	"B"
	"C"
	"D"
	"E"
	"F"
	"G"
]

Casting variables booleanas

columnas_bool = ["alone", "survived"]

titanic = titanic.with_columns([
    pl.col(col).cast(pl.Boolean) for col in columnas_bool
])

Resulado Casting

titanic.schema
Schema([('survived', Boolean),
        ('pclass', Enum(categories=['3', '2', '1'])),
        ('sex', Categorical(ordering='physical')),
        ('age', Float64),
        ('sibsp', Int64),
        ('parch', Int64),
        ('fare', Float64),
        ('embarked', Categorical(ordering='physical')),
        ('class', Categorical(ordering='physical')),
        ('who', Categorical(ordering='physical')),
        ('adult_male', Boolean),
        ('deck', Enum(categories=['A', 'B', 'C', 'D', 'E', 'F', 'G'])),
        ('embark_town', Categorical(ordering='physical')),
        ('alive', Categorical(ordering='physical')),
        ('alone', Boolean)])

Datos Faltantes (Missing data)

Métodos para Manejar datos faltantes en Polars:

# Declaracion de data frame con algunos datos faltantes
# NaN = Not a Number
df = pl.DataFrame({'A':[1, 2, None],
                   'B':[5, None, None],
                   'C':[1, 2 , 3],
                   'D':[None, None, None]})
df
shape: (3, 4)
ABCD
i64i64i64null
151null
2null2null
nullnull3null

Detectar si Faltan datos

# verificar cuales valores son NaN o nulos (Null)
# Verificar cuáles valores son nulos en todas las columnas
df.select([
    pl.col(col).is_null().alias(col) for col in df.columns
])
shape: (3, 4)
ABCD
boolboolboolbool
falsefalsefalsetrue
falsetruefalsetrue
truetruefalsetrue
# Verificar si hay datos faltantes por columna
df.select([
    pl.col(col).is_null().any().alias(col) for col in df.columns
])
shape: (1, 4)
ABCD
boolboolboolbool
truetruefalsetrue

Numero de datos faltantes

Calcular el numero de datos nulos que hay por columna

# Numero de datos faltantes por columna
df.null_count()
shape: (1, 4)
ABCD
u32u32u32u32
1203
df
shape: (3, 4)
ABCD
i64i64i64null
151null
2null2null
nullnull3null

Eliminar datos Faltantes

Se logra con el metodo .drop_nulls

# Eliminar todas las columnas que tengan todos los datos faltantes
# en este caso la columna D
df = df[[s.name for s in df if not (s.null_count() == df.height)]]
df
shape: (3, 3)
ABC
i64i64i64
151
2null2
nullnull3
# Eliminar todas las filas que tengan datos faltantes
df.drop_nulls()
shape: (1, 3)
ABC
i64i64i64
151

Reemplazar los datos faltantes

# Llenar los datos faltantes con el dato que nos interese
df.fill_null("Llenar Valores")# llenar los espacios con un string
                              # puede ser una palabra, numero , etc
shape: (3, 3)
ABC
i64i64i64
151
2null2
nullnull3
# Llenar los datos faltantes con el dato que nos interese
df.fill_null(value=99) # llenar los espacios con un numero
shape: (3, 3)
ABC
i64i64i64
151
2992
99993
# Llenar los datos faltantes con el promedio de esa columna
df.with_columns(
    pl.col("A").fill_null(pl.col("A").mean())
)
shape: (3, 3)
ABC
f64i64i64
1.051
2.0null2
1.5null3
# Llenar los datos faltantes con el promedio de cada columna
df.with_columns([
    pl.col(col).fill_null(pl.col(col).mean()) for col in df.columns
])
shape: (3, 3)
ABC
f64f64f64
1.05.01.0
2.05.02.0
1.55.03.0

Datos unicos (Unique Values)

# Crear un DataFrame
df = pl.DataFrame({
    'col1': [1, 2, 3, 4],
    'col2': [444, 555, 666, 444],
    'col3': ['abc', 'def', 'ghi', 'xyz']
})
df.head() # solamente mostrar los primeros elementos del dataframe
shape: (4, 3)
col1col2col3
i64i64str
1444"abc"
2555"def"
3666"ghi"
4444"xyz"
# valores unicos de la columna col2
df['col2'].unique()
shape: (3,)
col2
i64
444
555
666
# Numero de valores unicos en el dataframe
df['col2'].n_unique()
3
# contar cuanto se repiten cada uno de los valores
df['col2'].value_counts()
shape: (3, 2)
col2count
i64u32
6661
4442
5551

Datos Duplicados

Se puede borrar los registros que son exactamente iguales en todos los valores de las columnas

datos = {
    'nombre': ['Jaime', 'Juan', 'Roberto', 'Juan'],
    'edad': [18, 20, 22, 20],
    'trabajo': ['Asistente', 'Manager', 'Cientifico', 'Manager']
}
df_dup = pl.DataFrame(datos)
# método para detectar los datos duplicados
# me sirve para ver si existen registros duplicados
df_dup.is_duplicated()
shape: (4,)
bool
false
true
false
true
# Contar cuantos datos duplicados existen
df_dup.is_duplicated().sum()
2
# Para remover los datos duplicados
df_dup.unique()
shape: (3, 3)
nombreedadtrabajo
stri64str
"Juan"20"Manager"
"Roberto"22"Cientifico"
"Jaime"18"Asistente"
# El método drop_duplicates entrega el data frame sin duplicados
# Pero la función no es inplace, osea que el dataframe original sigue igual
df_dup
shape: (4, 3)
nombreedadtrabajo
stri64str
"Jaime"18"Asistente"
"Juan"20"Manager"
"Roberto"22"Cientifico"
"Juan"20"Manager"
df_dup = df_dup.unique(maintain_order=True)
df_dup
shape: (3, 3)
nombreedadtrabajo
stri64str
"Jaime"18"Asistente"
"Juan"20"Manager"
"Roberto"22"Cientifico"

Duplicados por Columna

Se pueden borrar los valores que se repitan solamente verificando la columna

# Crear un DataFrame
frame_datos = {
    'nombre': ['Jaime', 'Juan', 'Roberto', 'Juan'],
    'edad': [18, 20, 22, 21],
    'trabajo': ['Asistente', 'Manager', 'Cientifico', 'Profesor']
}
df_dup = pl.DataFrame(frame_datos)
df_dup
shape: (4, 3)
nombreedadtrabajo
stri64str
"Jaime"18"Asistente"
"Juan"20"Manager"
"Roberto"22"Cientifico"
"Juan"21"Profesor"

el valor de jason esta duplicado en la columna name

# recordar que estas funciones no son inplace
df_dup.unique(subset=['nombre'])
shape: (3, 3)
nombreedadtrabajo
stri64str
"Juan"20"Manager"
"Roberto"22"Cientifico"
"Jaime"18"Asistente"

Outliers

Una de las formas de eliminar los outliers es identificando cual sera el rango en el que queremos nuestros datos y limitar los datos entre ese rango

import seaborn as sns # importar la libreria seaborn
# cargar dataset del titanic
titanic = sns.load_dataset('titanic')

# Convertir dataframe pandas a Polars
titanic = pl.from_pandas(titanic)
titanic.head()
shape: (5, 15)
survivedpclasssexagesibspparchfareembarkedclasswhoadult_maledeckembark_townalivealone
i64i64strf64i64i64f64strcatstrboolcatstrstrbool
03"male"22.0107.25"S""Third""man"truenull"Southampton""no"false
11"female"38.01071.2833"C""First""woman"false"C""Cherbourg""yes"false
13"female"26.0007.925"S""Third""woman"falsenull"Southampton""yes"true
11"female"35.01053.1"S""First""woman"false"C""Southampton""yes"false
03"male"35.0008.05"S""Third""man"truenull"Southampton""no"true
edad = titanic['age']
edad
shape: (891,)
age
f64
22.0
38.0
26.0
35.0
35.0
27.0
19.0
null
26.0
32.0
# Cual es la edad maxima
edad.max()
80.0
# Cual es la edad Minima
edad.min()
0.42

Solo por hacer el ejercicio se determina que los outliers son valores que estan por fuera de los rangos de las edades entre 1 y 70 años

  1. Elemento de la lista
  2. Elemento de la lista

esto se puede hacer con el método .clip(lower_bound = Valor mas bajo, upper_bound = valor mas alto)

edad = edad.clip(lower_bound=1, upper_bound=70)
edad.max()
70.0
edad.min()
1.0

Tablas de Contingencia (two way tables)

Para lograr tablas de contingencia para variables categoricas, se puede lograr asi

# Crear Datos
raw_data = {
    'regiment': ['Nighthawks', 'Nighthawks', 'Nighthawks', 'Nighthawks', 'Dragoons', 'Dragoons', 'Dragoons', 'Dragoons', 'Scouts', 'Scouts', 'Scouts', 'Scouts'],
    'company': ['infantry', 'infantry', 'cavalry', 'cavalry', 'infantry', 'infantry', 'cavalry', 'cavalry','infantry', 'infantry', 'cavalry', 'cavalry'],
    'experience': ['veteran', 'rookie', 'veteran', 'rookie', 'veteran', 'rookie', 'veteran', 'rookie','veteran', 'rookie', 'veteran', 'rookie'],
    'name': ['Miller', 'Jacobson', 'Ali', 'Milner', 'Cooze', 'Jacon', 'Ryaner', 'Sone', 'Sloan', 'Piger', 'Riani', 'Ali'],
    'preTestScore': [4, 24, 31, 2, 3, 4, 24, 31, 2, 3, 2, 3],
    'postTestScore': [25, 94, 57, 62, 70, 25, 94, 57, 62, 70, 62, 70]}
df = pl.DataFrame(raw_data)
df
shape: (12, 6)
regimentcompanyexperiencenamepreTestScorepostTestScore
strstrstrstri64i64
"Nighthawks""infantry""veteran""Miller"425
"Nighthawks""infantry""rookie""Jacobson"2494
"Nighthawks""cavalry""veteran""Ali"3157
"Nighthawks""cavalry""rookie""Milner"262
"Dragoons""infantry""veteran""Cooze"370
"Dragoons""cavalry""rookie""Sone"3157
"Scouts""infantry""veteran""Sloan"262
"Scouts""infantry""rookie""Piger"370
"Scouts""cavalry""veteran""Riani"262
"Scouts""cavalry""rookie""Ali"370
# Tabla de contingencia por compañía y regimiento
df.group_by(['regiment', 'company']).len().pivot(index='regiment', on='company', values='len')
shape: (3, 3)
regimentinfantrycavalry
stru32u32
"Nighthawks"22
"Dragoons"22
"Scouts"22
# Tabla de contingencia de compañia y experiencia por regimiento

(df.group_by(['regiment','company', 'experience'])
    .len().pivot(index=['company', 'experience'], on='regiment', values='len'))
shape: (4, 5)
companyexperienceDragoonsScoutsNighthawks
strstru32u32u32
"infantry""rookie"111
"infantry""veteran"111
"cavalry""rookie"111
"cavalry""veteran"111

Aplicando funciones a cada elemento

Cuando necesitamos realizar algun computo a cada uno de los elementos de una columna Evite el uso de los métodos .apply(), .map() ya que en polars Estos métodos son mucho más lentos que usar expresiones nativas Polars Expressions.

Pueden encontrar ejemplos en internet con estos métodos pero no son la mejor opción.

Si no encuentra una solución usando expresiones puede usar .map_elements(), pero no es recomendado.

import polars as pl
# crear un dataframe
df = pl.DataFrame({'col1':[1,2,3,4],
                   'col2':[444,555,666,444],
                   'col3':['mama   ','  papa','   HIJO  ','HiJa'],
                   'col4':[True, False,True, False]})
df.head() # solamente mostrar los primeros elementos del dataframe
shape: (4, 4)
col1col2col3col4
i64i64strbool
1444"mama "true
2555" papa"false
3666" HIJO "true
4444"HiJa"false

Para aplicar algun calculo simple a cada valor de una columna es mejor hacerlo usando los metodos o expresiones de polars

df.with_columns(
    pl.col('col1')*2 + 4
    )
shape: (4, 4)
col1col2col3col4
i64i64strbool
6444"mama "true
8555" papa"false
10666" HIJO "true
12444"HiJa"false

Evitar de usar la siguiente opcion

#definición de función
def mult_suma(x: float)->float:
  """ Multiplica un número por 2 y le suma 4
  Args:
    x (float): número a operar
  Returns:
    (float): x*2 + 4

  """
  return x*2 + 4
# evitar usar esta solucion
df.with_columns(
    pl.col('col1').map_elements(mult_suma, return_dtype=pl.Float64)
    )
/tmp/ipykernel_12097/629502777.py:3: PolarsInefficientMapWarning:
Expr.map_elements is significantly slower than the native expressions API.
Only use if you absolutely CANNOT implement your logic otherwise.
Replace this expression...
  - pl.col("col1").map_elements(mult_suma)
with this one instead:
  + (pl.col("col1") * 2) + 4

  pl.col('col1').map_elements(mult_suma, return_dtype=pl.Float64)
shape: (4, 4)
col1col2col3col4
f64i64strbool
6.0444"mama "true
8.0555" papa"false
10.0666" HIJO "true
12.0444"HiJa"false

Polars hace sugerencias de como se puede modificar el codigo

Ejemplo: Operadores Logicos

df.with_columns(
    pl.col('col4') | True   # Operador or
    )
shape: (4, 4)
col1col2col3col4
i64i64strbool
1444"mama "true
2555" papa"true
3666" HIJO "true
4444"HiJa"true
df.with_columns(
    pl.col('col4') & False # operador and
    )
shape: (4, 4)
col1col2col3col4
i64i64strbool
1444"mama "false
2555" papa"false
3666" HIJO "false
4444"HiJa"false

Ejemplo: Operadores Comparacion

df.with_columns(
    pl.col('col3') == '  papa' # tiene dos espacios antes de la palabra papa
    )
shape: (4, 4)
col1col2col3col4
i64i64boolbool
1444falsetrue
2555truefalse
3666falsetrue
4444falsefalse
df.with_columns(
    pl.col('col3') != '  papa' # tiene dos espacios antes de la palabra papa
    )
shape: (4, 4)
col1col2col3col4
i64i64boolbool
1444truetrue
2555falsefalse
3666truetrue
4444truefalse
df.with_columns(
    pl.col('col1') > 3 # Mayor que
    )
shape: (4, 4)
col1col2col3col4
booli64strbool
false444"mama "true
false555" papa"false
false666" HIJO "true
true444"HiJa"false
df.with_columns(
    (pl.col('col1') >= 3).alias("comparacion") # Mayor o  igual que
    )
# metodo .alias() guarda los resultados en una nueva columna
shape: (4, 5)
col1col2col3col4comparacion
i64i64strboolbool
1444"mama "truefalse
2555" papa"falsefalse
3666" HIJO "truetrue
4444"HiJa"falsetrue
df.with_columns(
    pl.col('col1') < 3 # Menor que
    )
shape: (4, 4)
col1col2col3col4
booli64strbool
true444"mama "true
true555" papa"false
false666" HIJO "true
false444"HiJa"false
df.with_columns(
    pl.col('col1') <= 3 # Menor o igual que
    )
shape: (4, 4)
col1col2col3col4
booli64strbool
true444"mama "true
true555" papa"false
true666" HIJO "true
false444"HiJa"false

Ejemplo: Operaciones matematicas

df.with_columns(
    ((3*pl.col('col1') + pl.col('col2'))*2/27).alias('Resultado')
    )
shape: (4, 5)
col1col2col3col4Resultado
i64i64strboolf64
1444"mama "true33.111111
2555" papa"false41.555556
3666" HIJO "true50.0
4444"HiJa"false33.777778

Métodos en Polars

Todos los métodos de los dataframe de polars se pueden encontrar en:

https://docs.pola.rs/api/python/stable/reference/dataframe/index.html

Métodos Basicos de agregacion en Polars

Ejemplos simples de los métodos de agregacion de valores en los dataframe de polars

Para información completa de los métodosver:

https://docs.pola.rs/api/python/stable/reference/dataframe/aggregation.html

# Suma total de cada Columna, si es categorico los concatena
df.sum()
shape: (1, 4)
col1col2col3col4
i64i64stru32
102109null2
# método en una sola columna
df['col1'].sum()
10
# método en varias columnas
df[['col1','col2']].sum()
shape: (1, 2)
col1col2
i64i64
102109
# Valor Minimo cada Columna
df.min()
shape: (1, 4)
col1col2col3col4
i64i64strbool
1444" HIJO "false
# Valor Maximo cada Columna
df.max()
shape: (1, 4)
col1col2col3col4
i64i64strbool
4666"mama "true

métodos de información General

# Cargar la base de datos 'mpg' de la libreria seaborn
import seaborn as sns

# los datos se cargan en un dataframe
data = sns.load_dataset('mpg')

# Convertir a dataframe de polars
data = pl.from_pandas(data)
# Esquema de los datos del dataframe
data.schema
Schema([('mpg', Float64),
        ('cylinders', Int64),
        ('displacement', Float64),
        ('horsepower', Float64),
        ('weight', Int64),
        ('acceleration', Float64),
        ('model_year', Int64),
        ('origin', String),
        ('name', String)])
# tipos de datos por columna del dataframe
data.dtypes
[Float64, Int64, Float64, Float64, Int64, Float64, Int64, String, String]
data.head()
shape: (5, 9)
mpgcylindersdisplacementhorsepowerweightaccelerationmodel_yearoriginname
f64i64f64f64i64f64i64strstr
18.08307.0130.0350412.070"usa""chevrolet chevelle malibu"
15.08350.0165.0369311.570"usa""buick skylark 320"
18.08318.0150.0343611.070"usa""plymouth satellite"
16.08304.0150.0343312.070"usa""amc rebel sst"
17.08302.0140.0344910.570"usa""ford torino"
data.describe() # ver datos estadisticos de las columnas numericas
shape: (9, 10)
statisticmpgcylindersdisplacementhorsepowerweightaccelerationmodel_yearoriginname
strf64f64f64f64f64f64f64strstr
"count"398.0398.0398.0392.0398.0398.0398.0"398""398"
"null_count"0.00.00.06.00.00.00.0"0""0"
"mean"23.5145735.454774193.425879104.4693882970.42462315.5680976.01005nullnull
"std"7.8159841.701004104.26983838.49116846.8417742.7576893.697627nullnull
"min"9.03.068.046.01613.08.070.0"europe""amc ambassador brougham"
"25%"17.54.0104.075.02223.013.873.0nullnull
"50%"23.04.0151.094.02807.015.576.0nullnull
"75%"29.08.0262.0125.03609.017.279.0nullnull
"max"46.68.0455.0230.05140.024.882.0"usa""vw rabbit custom"
# Descripcion de una sola columna
data['cylinders'].describe()
shape: (9, 2)
statisticvalue
strf64
"count"398.0
"null_count"0.0
"mean"5.454774
"std"1.701004
"min"3.0
"25%"4.0
"50%"4.0
"75%"8.0
"max"8.0
data['mpg'].describe()
shape: (9, 2)
statisticvalue
strf64
"count"398.0
"null_count"0.0
"mean"23.514573
"std"7.815984
"min"9.0
"25%"17.5
"50%"23.0
"75%"29.0
"max"46.6
data['cylinders'].dtype
Int64

Estadistica Descriptiva

Para las Series los métodos de Estadistica Descriptiva, de medida central, simetria, momentos, etc. para mas informacion:

https://docs.pola.rs/api/python/stable/reference/series/aggregation.html

Los calculos de las medidas estadisticas se hacen siempre en columnas, es algo predeterminado.

# Se tomara la columna 'mpg' para realizar los calculos
X = data['mpg']
type(X)
polars.series.series.Series

Medidas de centralizacion

estos metodos se aplican sobre Series o dataframe de polars

Media

# Media Aritmetica
X.mean()
23.514572864321607
# Media aritmetica en un dataframe
data.mean()
shape: (1, 9)
mpgcylindersdisplacementhorsepowerweightaccelerationmodel_yearoriginname
f64f64f64f64f64f64f64strstr
23.5145735.454774193.425879104.4693882970.42462315.5680976.01005nullnull

Mediana

# Mediana
X.median()
23.0
# Mediana en un dataframe
data.median()
shape: (1, 9)
mpgcylindersdisplacementhorsepowerweightaccelerationmodel_yearoriginname
f64f64f64f64f64f64f64strstr
23.04.0148.593.52803.515.576.0nullnull

Maximo y Minimo

# Maximo
X.max()
46.6
# Maximo en un dataframe
data.max()
shape: (1, 9)
mpgcylindersdisplacementhorsepowerweightaccelerationmodel_yearoriginname
f64i64f64f64i64f64i64strstr
46.68455.0230.0514024.882"usa""vw rabbit custom"
# Minimo
X.min()
9.0
# Minimo en un dataframe
data.min()
shape: (1, 9)
mpgcylindersdisplacementhorsepowerweightaccelerationmodel_yearoriginname
f64i64f64f64i64f64i64strstr
9.0368.046.016138.070"europe""amc ambassador brougham"

Moda

# Moda
X.mode()
shape: (1,)
mpg
f64
13.0

Cuartiles

# Valores de los cuartiles
# en este ejemplo del primer cuartil, el 25%
X.quantile(.25)
17.5
# Valores de los cuartiles en un dataframe
data.quantile(0.25)
shape: (1, 9)
mpgcylindersdisplacementhorsepowerweightaccelerationmodel_yearoriginname
f64f64f64f64f64f64f64strstr
17.54.0104.075.02223.013.873.0nullnull

Medidas de dispersion

Varianza

# Varianza
X.var() #unbiased Normalized by N-1 by default.
61.08961077427439
# Varianza en un dataframe
data.var()
shape: (1, 9)
mpgcylindersdisplacementhorsepowerweightaccelerationmodel_yearoriginname
f64f64f64f64f64f64f64strstr
61.0896112.89341510872.1991521481.569393717140.9905267.60484813.672443nullnull

Desviacion Estandard

# Desviacion tipica o desviacion estandard
X.std()
7.8159843125657815
# Desviacion tipica o desviacion estandard en un dataframe
data.std()
shape: (1, 9)
mpgcylindersdisplacementhorsepowerweightaccelerationmodel_yearoriginname
f64f64f64f64f64f64f64strstr
7.8159841.701004104.26983838.49116846.8417742.7576893.697627nullnull

Coeficiente de Variacion

# Coeficiente de Variacion
X.std() / X.mean()
0.332388955464502

Medidas de Asimetria

Asimetria de Fisher (skewness)

La asimetría es la medida que indica la simetría de la distribución de una variable respecto a la media aritmética, sin necesidad de hacer la representación gráfica. Los coeficientes de asimetría indican si hay el mismo número de elementos a izquierda y derecha de la media.

Existen tres tipos de curva de distribución según su asimetría:

  • Asimetría negativa: la cola de la distribución se alarga para valores inferiores a la media.
  • Simétrica: hay el mismo número de elementos a izquierda y derecha de la media. En este caso, coinciden la media, la mediana y la moda. La distribución se adapta a la forma de la campana de Gauss, o distribución normal.
  • Asimetría positiva: la cola de la distribución se alarga para valores superiores a la media.

#unbiased skew, Normalized by N-1
X.skew()
0.45534192556309294

Curtosis

Esta medida determina el grado de concentración que presentan los valores en la región central de la distribución. Por medio del Coeficiente de Curtosis, podemos identificar si existe una gran concentración de valores (Leptocúrtica), una concentración normal (Mesocúrtica) ó una baja concentración (Platicúrtica).

# unbiased kurtosis over requested axis using Fisher's definition
X.kurtosis()
-0.5194245405990436

Covarianza

Entre Series

data.select(pl.cov('mpg','displacement'))
shape: (1, 1)
mpg
f64
-655.402318

Correlacion

Metodos:

  • pearson (predeterminado)
  • kendall
  • spearman

Entre Series

data.select(pl.corr('mpg','displacement')) # Pearson que es el predeterminado
shape: (1, 1)
mpg
f64
-0.804203
data.select(pl.corr('mpg','displacement', method='spearman')) # método spearman
shape: (1, 1)
mpg
f64
-0.855692
data.select(pl.corr('mpg','displacement', method='pearson'))  # método pearson
shape: (1, 1)
mpg
f64
-0.804203

Dataframe

data.select(pl.col(pl.Float64)).corr()
shape: (4, 4)
mpgdisplacementhorsepoweracceleration
f64f64f64f64
1.0-0.804203NaN0.420289
-0.8042031.0NaN-0.543684
NaNNaNNaNNaN
0.420289-0.543684NaN1.0

(Sorting) Ordenar un DataFrame:

df
shape: (4, 4)
col1col2col3col4
i64i64strbool
1444"mama "true
2555" papa"false
3666" HIJO "true
4444"HiJa"false
# ordenar el dataframe de menor a mayor basado en col2
df.sort('col2')
shape: (4, 4)
col1col2col3col4
i64i64strbool
1444"mama "true
4444"HiJa"false
2555" papa"false
3666" HIJO "true
# ordenar el dataframe de mayor a menor basado en col2
df.sort('col2', descending=True)
shape: (4, 4)
col1col2col3col4
i64i64strbool
3666" HIJO "true
2555" papa"false
1444"mama "true
4444"HiJa"false

métodos de strings

Los métodos de los strings se pueden usar en polars de forma vectorizada

https://docs.pola.rs/api/python/stable/reference/series/string.html

Nota: Estos metodos son para Series

Algunos ejemplos son:

df
shape: (4, 4)
col1col2col3col4
i64i64strbool
1444"mama "true
2555" papa"false
3666" HIJO "true
4444"HiJa"false
# Estos métodos solo funcionan  por columna (Series)
# se puede verificar el error
# df.str.to_lowercase()

Algunos ejemplos de métodos en strings en las columnas

# convertir strings en minuscula
df['col3'].str.to_lowercase()
shape: (4,)
col3
str
"mama "
" papa"
" hijo "
"hija"
# usando expresiones
df.with_columns(
    pl.col('col3').str.to_lowercase()
    )

# observar que retorna todo el dataframe
shape: (4, 4)
col1col2col3col4
i64i64strbool
1444"mama "true
2555" papa"false
3666" hijo "true
4444"hija"false
# convertir strings en Mayuscula
df['col3'].str.to_uppercase()
shape: (4,)
col3
str
"MAMA "
" PAPA"
" HIJO "
"HIJA"
# Eliminar los espacios de los strings inicio y final
df['col3'].str.strip_chars(' ')
shape: (4,)
col3
str
"mama"
"papa"
"HIJO"
"HiJa"

Transformacion de Variables

  • Crear variables Dummy: convertir de categoría a númerica
  • Discretización o Binning: convertir de número a categoría

Columnas Dummy

Convertir variables categoricas a numericas

# crear datos categoricos
raw_data = {'first_name': ['Jason', 'Molly', 'Tina', 'Jake', 'Amy'],
        'last_name': ['Miller', 'Jacobson', 'Ali', 'Milner', 'Cooze'],
        'sex': ['male', 'female', 'male', 'female', 'female']}
df = pl.DataFrame(raw_data)
df
shape: (5, 3)
first_namelast_namesex
strstrstr
"Jason""Miller""male"
"Molly""Jacobson""female"
"Tina""Ali""male"
"Jake""Milner""female"
"Amy""Cooze""female"
# Crear un set de variables dummy para la columna sex
df_sex = df.to_dummies(columns=['sex'])
df_sex
shape: (5, 4)
first_namelast_namesex_femalesex_male
strstru8u8
"Jason""Miller"01
"Molly""Jacobson"10
"Tina""Ali"01
"Jake""Milner"10
"Amy""Cooze"10

Discretización o Binning

Conversion de Numerica a Categorica

https://docs.pola.rs/api/python/stable/reference/series/api/polars.Series.cut.html

# Dividir los datos en 3 rangos iguales (categorías)
data_series = pl.Series("notas", [3.5, 2.8, 1, 5, 3, 4, 0, 4.4, 2, 3])

data_series.cut([2.9, 3.9 ], labels=["Malo", "Regular", "Bien"])
shape: (10,)
notas
cat
"Regular"
"Malo"
"Malo"
"Bien"
"Regular"
"Bien"
"Malo"
"Bien"
"Malo"
"Regular"
data_edad = pl.Series("edad", [1, 2, 4 , 10 , 35 , 25 , 60 , 23, 14])
data_edad.cut([2,10,18], labels=["Bebe","Niño", "Adolescente", "Adulto"])
shape: (9,)
edad
cat
"Bebe"
"Bebe"
"Niño"
"Niño"
"Adulto"
"Adulto"
"Adulto"
"Adulto"
"Adolescente"

Leer y guardar Datos (Data Input and Output)

Polars puede leer una variedad de tipos de archivos usando sus métodos pl.read_ mas informacion

  • CSV
  • Parquet (Formato recomendado)
  • Excel
  • Json
  • Html
  • Database
  • Cloud storage
  • Google Big Query
  • Hugging Face

Nota: estos se ejemplos se hacen con archivos previamente creados para la demostracion

CSV File (comma-separated values)

Este es un formato muy común para compartir datos. Los archivos CSV son archivos de texto, sin formato, que usan comas para separar filas. Sus desventajas son que no especifican tipos de datos, no admiten datos faltantes, entre otros.

No es un formato ideal para compartir datos.

CSV Input

# Leer archivos separados por comas, extension .csv
df_supermarket_csv = pl.read_csv('supermarkets.csv')
df_supermarket_csv.sample(5)
shape: (5, 7)
IDAddressCityStateCountryNameEmployees
i64strstrstrstrstri64
4"3995 23rd St""San Francisco""CA 94114""USA""Ben's Shop"10
3"332 Hill St""San Francisco""California 94114""USA""Super River"25
2"735 Dolores St""San Francisco""CA 94119""USA""Bready Shop"15
6"551 Alvarado St""San Francisco""CA 94114""USA""Richvalley"20
1"3666 21st St""San Francisco""CA 94114""USA""Madeira"8

CSV Output

# Grabar el dataframe como archivo separado por comas
df_supermarket_csv.write_csv('example_out.csv')

Parquet

Este es un formato de archivo binario de columna que es muy bueno para compartir datos. Es muy eficiente en el espacio y el tiempo de lectura.

Es el formato recomendado para compartir datos.

pip install pyarrow

Parquet Input

# Leer archivos separados por comas, extension .csv
df_supermarket_parquet = pl.read_parquet('supermarkets.parquet')
df_supermarket_parquet.sample(5)
shape: (5, 7)
IDAddressCityStateCountryNameEmployees
i64strstrstrstrstri64
3"332 Hill St""San Francisco""California 94114""USA""Super River"25
6"551 Alvarado St""San Francisco""CA 94114""USA""Richvalley"20
5"1056 Sanchez St""San Francisco""California""USA""Sanchez"12
2"735 Dolores St""San Francisco""CA 94119""USA""Bready Shop"15
4"3995 23rd St""San Francisco""CA 94114""USA""Ben's Shop"10

Parquet Output

# Grabar el dataframe como archivo parquet
df_supermarket_parquet.write_parquet('example_out.parquet')

Excel

Polars puede leer y escribir archivos de Excel, tenga en cuenta que esto solo importa datos. No fórmulas o imágenes, que tengan imágenes o macros pueden hacer que este método read_excel se bloquee.

Es necesario instalar algunas librerias fastexcel y xlsxwriter

pip install fastexcel

pip install xlsxwriter

Excel Input

#!pip install fastexcel
# leer un archivo de excel
df_supermarket_excel = pl.read_excel(
    "supermarkets.xlsx",
    sheet_name='Ark1',  # leer la hoja llamada Ark1
)
df_supermarket_excel.sample(5)
shape: (5, 7)
IDAddressCityStateCountrySupermarket NameNumber of Employees
i64strstrstrstrstri64
4"3995 23rd St""San Francisco""CA 94114""USA""Ben's Shop"10
2"735 Dolores St""San Francisco""CA 94119""USA""Bready Shop"15
5"1056 Sanchez St""San Francisco""California""USA""Sanchez"12
6"551 Alvarado St""San Francisco""CA 94114""USA""Richvalley"20
3"332 Hill St""San Francisco""California 94114""USA""Super River"25

Excel Output

#!pip install xlsxwriter
df_supermarket_excel.write_excel("Excel_Sample_out.xlsx", worksheet="Hoja1")
<xlsxwriter.workbook.Workbook at 0x723f182c5e40>

JSON

JSON (JavaScript Object Notation - Notación de Objetos de JavaScript) es un formato ligero de intercambio de datos. Leerlo y escribirlo es simple para humanos, mientras que para las máquinas es simple interpretarlo y generarlo.

Json Input

data from: https://pythonhow.com/media/data/supermarkets.json

# los archivos pueden estar en un link de internet
df_supermarket_json = pl.read_json("supermarkets.json")
df_supermarket_json.sample(5)
shape: (5, 7)
IDAddressCityStateCountryNameEmployees
i64strstrstrstrstri64
5"1056 Sanchez St""San Francisco""California""USA""Sanchez"12
1"3666 21st St""San Francisco""CA 94114""USA""Madeira"8
2"735 Dolores St""San Francisco""CA 94119""USA""Bready Shop"15
4"3995 23rd St""San Francisco""CA 94114""USA""Ben's Shop"10
6"551 Alvarado St""San Francisco""CA 94114""USA""Richvalley"20

Json output

# Para grabar
df_supermarket_json.write_json("Salida.json")

Referencias

Phd. Jose R. Zapata