/ BLOG, PANDAS, PYTHON

Sidetable, el beffo de pandas

Sidetable es una biblioteca de python que agrega métodos a los DataFrames de pandas para facilitar la exploración de datos. Según Chris Moffit (el creador de sidetable), en el repositorio de GitHub del proyecto, sidetables es como una versión más chida de .value_counts() y .crosstab() juntos para facilitar la creación de tablas resumen (? summary tables). Repositorio de sidetable en GitHub

Este tutorial va a ser una rápida introducción a sidetable para demostrar sus habilidades. Estaremos utilizando el conjunto de datos palmerpenguins que tiene datos sobre 💖PINGÜINOS💖 que es una gran alternativa al dataset iris que es comúnmente usado en estos tutoriales por que 1) los datos vienen de un estadístico racista que los publicó en una publicación sobre eugenesia🤮

Para más información de los datos visita su página oficial allisonhorst.github.io/palmerpenguis/

Para nuestra suerte la biblioteca seaborn y su complemento seaborn-data nos ayudan a obtener los datos rápidamente.

Prepareishon

Obvis microbis tines que tener instalado pandas y sidetable. en este tutorial vamos a usar seaborn para acceder los datos entonces también necesitarás instalarlo.

pip install -U pandas sidetable seaborn

la U es de --upgrade, significa que actualiza las bibliotecas si ya las tienes instaladas.

import seaborn as sns
import pandas as pd
import sidetable

print(f"Versión de seaborn: {sns.__version__}")
print(f"Versión de pandas: {pd.__version__}")
print(f"Versión de sidetable: {sidetable.__version__}")
Versión de seaborn: 0.9.0
Versión de pandas: 1.0.5
Versión de sidetable: 0.5.0
datos = sns.load_dataset('penguins')

datos.head()
species island culmen_length_mm culmen_depth_mm flipper_length_mm body_mass_g sex
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 MALE
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 FEMALE
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 FEMALE
3 Adelie Torgersen NaN NaN NaN NaN NaN
4 Adelie Torgersen 36.7 19.3 193.0 3450.0 FEMALE

Exploreishon

Dos métodos para explorar un DataFrame rápidamente son .info() y .describe() los cuales te muestran lo siguiente

datos.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 344 entries, 0 to 343
Data columns (total 7 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   species            344 non-null    object 
 1   island             344 non-null    object 
 2   culmen_length_mm   342 non-null    float64
 3   culmen_depth_mm    342 non-null    float64
 4   flipper_length_mm  342 non-null    float64
 5   body_mass_g        342 non-null    float64
 6   sex                333 non-null    object 
dtypes: float64(4), object(3)
memory usage: 18.9+ KB
datos.describe()
culmen_length_mm culmen_depth_mm flipper_length_mm body_mass_g
count 342.000000 342.000000 342.000000 342.000000
mean 43.921930 17.151170 200.915205 4201.754386
std 5.459584 1.974793 14.061714 801.954536
min 32.100000 13.100000 172.000000 2700.000000
25% 39.225000 15.600000 190.000000 3550.000000
50% 44.450000 17.300000 197.000000 4050.000000
75% 48.500000 18.700000 213.000000 4750.000000
max 59.600000 21.500000 231.000000 6300.000000

.info() te muestra una tabla con las columnas de tu DataFrame, la cuenta de valores no nulos y el dtype o data type de los datos de la columna. Mientras que describe() te muestra estadísticas de tus columnas numéricas. La cuenta, el promedio, la deviación estandar, el mínimo y máximo y los percentiles 25, 50 (la mediana) y 75.

Valores nulos

Lo que yo hacía antes era utilizar el método .isna() o .isnull() en el DataFrame encadenado a .sum() para ver cuantos valores nulos tenía cada columna

datos.isna().sum()
species               0
island                0
culmen_length_mm      2
culmen_depth_mm       2
flipper_length_mm     2
body_mass_g           2
sex                  11
dtype: int64

Y luego si quería saber que porcentage es del total era hacer algo así

datos.isna().sum() / len(datos)
species              0.000000
island               0.000000
culmen_length_mm     0.005814
culmen_depth_mm      0.005814
flipper_length_mm    0.005814
body_mass_g          0.005814
sex                  0.031977
dtype: float64

ya que len(datos) te da el "largo" del DataFrame, osea el número de filas.

sidetable simplifica esto de la siguiente manera

datos.stb.missing()
Missing Total Percent
sex 11 344 0.031977
culmen_length_mm 2 344 0.005814
culmen_depth_mm 2 344 0.005814
flipper_length_mm 2 344 0.005814
body_mass_g 2 344 0.005814
species 0 344 0.000000
island 0 344 0.000000

y hasta lo enchula

datos.stb.missing(style = True)
Missing Total Percent
sex 11 344 3.20%
culmen_length_mm 2 344 0.58%
culmen_depth_mm 2 344 0.58%
flipper_length_mm 2 344 0.58%
body_mass_g 2 344 0.58%
species 0 344 0.00%
island 0 344 0.00%

La mágia de sidetable es que agrega un accessor a los DataFrames de pandas. Al hacer import sidetable estás habilitando el accessor .stb. Así es como se integra a tu flujo de trabajo.

Frecuencias

El siguiente método incluido en sidetable es .freq() que es como un .value_counts() súper poderoso.

datos['island'].value_counts()
Biscoe       168
Dream        124
Torgersen     52
Name: island, dtype: int64
datos['island'].value_counts(normalize = True)
Biscoe       0.488372
Dream        0.360465
Torgersen    0.151163
Name: island, dtype: float64
datos.stb.freq(['island'])
island Count Percent Cumulative Count Cumulative Percent
0 Biscoe 168 0.488372 168 0.488372
1 Dream 124 0.360465 292 0.848837
2 Torgersen 52 0.151163 344 1.000000

Este método también tiene el parametro style para limpiar la columna de porcentages.

datos.stb.freq(['island'], style = True)
island Count Percent Cumulative Count Cumulative Percent
0 Biscoe 168 48.84% 168 48.84%
1 Dream 124 36.05% 292 84.88%
2 Torgersen 52 15.12% 344 100.00%

Algo que me encantó de esta biblioteca es que crea también estos porcentages y cuentas cumulativas. En este caso no agrega mucho contexto pero cuando trabajo con datos censales se me hace muy útil.

Habrás notado que le pasamos una lista de columnas (aunque haya sido una lista de 1. De hecho, el parámetro de la función .freq() se llama cols en plural. Veamos que sucede si le pasamos más de una columna.

datos.stb.freq(['island', 'species', 'sex'], style = True)
island species sex Count Percent Cumulative Count Cumulative Percent
0 Biscoe Gentoo MALE 61 18.32% 61 18.32%
1 Biscoe Gentoo FEMALE 58 17.42% 119 35.74%
2 Dream Chinstrap MALE 34 10.21% 153 45.95%
3 Dream Chinstrap FEMALE 34 10.21% 187 56.16%
4 Dream Adelie MALE 28 8.41% 215 64.56%
5 Dream Adelie FEMALE 27 8.11% 242 72.67%
6 Torgersen Adelie FEMALE 24 7.21% 266 79.88%
7 Torgersen Adelie MALE 23 6.91% 289 86.79%
8 Biscoe Adelie MALE 22 6.61% 311 93.39%
9 Biscoe Adelie FEMALE 22 6.61% 333 100.00%

para lograr algo similar puro pandas tendría que hacer algo como

datos.groupby(['island', 'species', 'sex'])['culmen_length_mm'].count().to_frame().reset_index()
island species sex culmen_length_mm
0 Biscoe Adelie FEMALE 22
1 Biscoe Adelie MALE 22
2 Biscoe Gentoo FEMALE 58
3 Biscoe Gentoo MALE 61
4 Dream Adelie FEMALE 27
5 Dream Adelie MALE 28
6 Dream Chinstrap FEMALE 34
7 Dream Chinstrap MALE 34
8 Torgersen Adelie FEMALE 24
9 Torgersen Adelie MALE 23

guardar este DataFrame con un nombre nuevo y crearle nuevas columnas. Con sidetable logro esto con un sólo comando.

Esa es la mágia de sidetable en mi opinión. No es que este ofreciendote funcionalidades que no existían en pandas. Lo que hace es hacerlas más accesibles 💖

Este método .freq() también tiene el parámetro thresh que sirve para agrupar valores hasta cierto valor cumulativo.

datos.stb.freq(['island', 'species'], style = True)
island species Count Percent Cumulative Count Cumulative Percent
0 Biscoe Gentoo 124 36.05% 124 36.05%
1 Dream Chinstrap 68 19.77% 192 55.81%
2 Dream Adelie 56 16.28% 248 72.09%
3 Torgersen Adelie 52 15.12% 300 87.21%
4 Biscoe Adelie 44 12.79% 344 100.00%
datos.stb.freq(['island', 'species'], style = True, thresh = .6)
island species Count Percent Cumulative Count Cumulative Percent
0 Biscoe Gentoo 124 36.05% 124 36.05%
1 Dream Chinstrap 68 19.77% 192 55.81%
2 Others Others 152 44.19% 344 100.00%

Lo que sidetable esta haciendo es mostrar tus valores hasta que llegan a tu thresh o límite. En este caso, muestra todos los valores que van sumando hasta llegar a un valor cumulativo de 60% y los demás valores se agrupan bajo la etiqueta Others (la cual también puedes modificar).

datos.stb.freq(['island', 'species'], style = True, thresh = .6, other_label = "baia baia tacubaya")
island species Count Percent Cumulative Count Cumulative Percent
0 Biscoe Gentoo 124 36.05% 124 36.05%
1 Dream Chinstrap 68 19.77% 192 55.81%
2 baia baia tacubaya baia baia tacubaya 152 44.19% 344 100.00%

Subtotal

El tercer método que sidetable ofrece es .subtotal() que te permite, como su nombre lo indica, crear subtotales de tus datos agrupados.

datos.stb.subtotal()
species island culmen_length_mm culmen_depth_mm flipper_length_mm body_mass_g sex
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 MALE
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 FEMALE
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 FEMALE
3 Adelie Torgersen NaN NaN NaN NaN NaN
4 Adelie Torgersen 36.7 19.3 193.0 3450.0 FEMALE
... ... ... ... ... ... ... ...
340 Gentoo Biscoe 46.8 14.3 215.0 4850.0 FEMALE
341 Gentoo Biscoe 50.4 15.7 222.0 5750.0 MALE
342 Gentoo Biscoe 45.2 14.8 212.0 5200.0 FEMALE
343 Gentoo Biscoe 49.9 16.1 213.0 5400.0 MALE
Grand Total NaN NaN 15021.3 5865.7 68713.0 1437000.0 NaN

345 rows × 7 columns

Puedes ver como agrega la fila Grand total a tu DataFrame. No es como que digas hijole que bruto que útil agregar un total a esta tabla pero lo más interesante aparece cuando aplicas .subtotal() a un objeto .groupby()

datos.groupby(['species', 'sex'])[['island']].count()
island
species sex
Adelie FEMALE 73
MALE 73
Chinstrap FEMALE 34
MALE 34
Gentoo FEMALE 58
MALE 61
datos.groupby(['species', 'sex'])[['island']].count().stb.subtotal()
island
species sex
Adelie FEMALE 73
MALE 73
Adelie - subtotal 146
Chinstrap FEMALE 34
MALE 34
Chinstrap - subtotal 68
Gentoo FEMALE 58
MALE 61
Gentoo - subtotal 119
Grand Total 333

Para hacer algo así en pandas tendrías que hacer algo como

datos.pivot_table(index=['species', 'sex',], values="culmen_length_mm", aggfunc='count', margins = True)
culmen_length_mm
species sex
Adelie FEMALE 73
MALE 73
Chinstrap FEMALE 34
MALE 34
Gentoo FEMALE 58
MALE 61
All 333

Esas son las 3 funcionalidades que sidetable agrega a pandas: .missing(), .freq() y .subtotal().

Personalmente, voy a utilizar sidetable en todos los cursos y talleres que imparta de ahora en adelante. Ofrece el poder de pandas pero más accesible.


Bonus

Aunque me encanten los pingüinos la verdad es que yo no uso ese tipo de datos en mi día a día así que aquí hay un ejemplo de una tabla que produciría en el trabajo.

Para este ejercicio voy a utilizar pypums una biblioteca mia que te permite descargar datos del censo de estados unidos de manera automatizable con python.

import pandas as pd
import sidetable
from pypums import ACS
california = ACS(state = 'california', year = 2017)
data = california.as_dataframe()
data.head()
RT SERIALNO DIVISION SPORDER PUMA REGION ST ADJINC PWGTP AGEP ... PWGTP71 PWGTP72 PWGTP73 PWGTP74 PWGTP75 PWGTP76 PWGTP77 PWGTP78 PWGTP79 PWGTP80
0 P 2017000000014 9 1 6703 4 6 1011189 59 45 ... 64 63 55 17 100 65 62 68 17 101
1 P 2017000000014 9 2 6703 4 6 1011189 57 39 ... 51 54 59 19 95 53 56 62 18 97
2 P 2017000000014 9 3 6703 4 6 1011189 54 15 ... 50 62 52 14 99 53 60 60 16 90
3 P 2017000000017 9 1 110 4 6 1011189 75 34 ... 140 73 127 67 21 73 27 117 65 113
4 P 2017000000017 9 2 110 4 6 1011189 71 33 ... 116 75 127 73 19 79 25 110 72 123

5 rows × 286 columns

Usando sidetable puedo encontrar estadísticas sobre la población de California rápidamente. Algo que me gusta mucho de sidetable es que puedo agregar el parámetro value para pasarle una columna que va a utilizar para la agregación de mis datos. En este caso, uso PWGTP (person weight) o factor de expansión. Esto es "lo que vale" cada observación en mis datos ya que estos datos son una muestra de la población total de estados unidos (es la American Community Survey que encuesta más o menos 1% de la población anualmente).

data.stb.freq(['SEX'], value = 'PWGTP', style = True)
SEX PWGTP Percent Cumulative PWGTP Cumulative Percent
0 2 19,891,993 50.31% 19,891,993 50.31%
1 1 19,644,660 49.69% 39,536,653 100.00%

Algo que yo he hecho muchas veces en mi trabajo anterior era investigar cuantas personas en California de X edad son inmigrantes, por ejemplo. Este número serviría de denominador en otras estadísticas.

Ya que tenemos los datos, agrupo la columna AGEP que tiene los valores de las edades.

mask_17 = data['AGEP'] < 18
data.loc[mask_17, 'AGE_BIN'] = '0 - 17'

# 18 - 25
mask_25 = (data['AGEP'] >= 18) & (data['AGEP'] <= 25)
data.loc[mask_25, 'AGE_BIN'] = '18 - 25'

# 26 - 35
mask_35 = (data['AGEP'] >= 26) & (data['AGEP'] <= 35)
data.loc[mask_35, 'AGE_BIN'] = '26 - 35'

# 36 - 45
mask_45 = (data['AGEP'] >= 36) & (data['AGEP'] <= 45)
data.loc[mask_45, 'AGE_BIN'] = '36 - 45'

# 46 - 55
mask_55 = (data['AGEP'] >= 46) & (data['AGEP'] <= 55)
data.loc[mask_55, 'AGE_BIN'] = '46 - 55'

# 56 - 65
mask_65 = (data['AGEP'] >= 56) & (data['AGEP'] <= 65)
data.loc[mask_65, 'AGE_BIN'] = '56 - 65'

# 65+
mask_65plus = (data['AGEP'] >= 66)
data.loc[mask_65plus, 'AGE_BIN'] = '65+'
age_bins = [
    '0 - 17',
    '18 - 25',
    '26 - 35',
    '36 - 45',
    '46 - 55',
    '56 - 65',
    '65+',
]
data['AGE_BIN'] = pd.Categorical(data['AGE_BIN'], categories = age_bins, ordered = True)

Y ya podemos ver por ejemplo, cuantos hombres y mujeres (abajo con el binarismo 👎) hay en cada grupo.

data.stb.freq(['SEX', 'AGE_BIN'], value = 'PWGTP', style = True)
SEX AGE_BIN PWGTP Percent Cumulative PWGTP Cumulative Percent
0 1 0 - 17 4,631,530 11.71% 4,631,530 11.71%
1 2 0 - 17 4,418,806 11.18% 9,050,336 22.89%
2 1 26 - 35 3,059,406 7.74% 12,109,742 30.63%
3 2 26 - 35 2,893,143 7.32% 15,002,885 37.95%
4 2 65+ 2,840,857 7.19% 17,843,742 45.13%
5 1 36 - 45 2,607,669 6.60% 20,451,411 51.73%
6 2 46 - 55 2,600,194 6.58% 23,051,605 58.30%
7 2 36 - 45 2,566,590 6.49% 25,618,195 64.80%
8 1 46 - 55 2,563,516 6.48% 28,181,711 71.28%
9 2 56 - 65 2,409,415 6.09% 30,591,126 77.37%
10 1 18 - 25 2,293,020 5.80% 32,884,146 83.17%
11 1 65+ 2,252,083 5.70% 35,136,229 88.87%
12 1 56 - 65 2,237,436 5.66% 37,373,665 94.53%
13 2 18 - 25 2,162,988 5.47% 39,536,653 100.00%

Para limpiar los datos un poco (para presentarlos en alguna junta o algo) puedo cambiar los valores (que obtuve del diccionario de datos: https://www2.census.gov/programs-surveys/acs/tech_docs/pums/data_dict/PUMS_Data_Dictionary_2018.pdf?#)

data_dict_sex = {
    1: 'Male',
    2: 'Female',
}

data_dict_cit = {
    1: "Born in the U.S.",
    2: "Born in Puerto Rico, Guam, the U.S. Virgin Islands, or the Northern Marianas",
    3: "Born abroad of American parent(s)",
    4: "U.S. citizen by naturalization",
    5: "Not a citizen of the U.S.",
}
data['SEX'] = data['SEX'].map(data_dict_sex)
data['CIT'] = data['CIT'].map(data_dict_cit)
data['CIT'] = pd.Categorical(data['CIT'], categories = data_dict_cit.values(), ordered = True)

¡Y listo!

data.stb.freq(['CIT', 'AGE_BIN', 'SEX'], value = 'PWGTP', style = True, sort_cols=True)
CIT AGE_BIN SEX PWGTP Percent Cumulative PWGTP Cumulative Percent
0 Born in the U.S. 0 - 17 Female 4,157,924 10.52% 4,157,924 10.52%
1 Born in the U.S. 0 - 17 Male 4,346,506 10.99% 8,504,430 21.51%
2 Born in the U.S. 18 - 25 Female 1,813,692 4.59% 10,318,122 26.10%
3 Born in the U.S. 18 - 25 Male 1,923,610 4.87% 12,241,732 30.96%
4 Born in the U.S. 26 - 35 Female 2,004,228 5.07% 14,245,960 36.03%
5 Born in the U.S. 26 - 35 Male 2,166,221 5.48% 16,412,181 41.51%
6 Born in the U.S. 36 - 45 Female 1,395,617 3.53% 17,807,798 45.04%
7 Born in the U.S. 36 - 45 Male 1,499,406 3.79% 19,307,204 48.83%
8 Born in the U.S. 46 - 55 Female 1,428,899 3.61% 20,736,103 52.45%
9 Born in the U.S. 46 - 55 Male 1,436,241 3.63% 22,172,344 56.08%
10 Born in the U.S. 56 - 65 Female 1,476,071 3.73% 23,648,415 59.81%
11 Born in the U.S. 56 - 65 Male 1,405,794 3.56% 25,054,209 63.37%
12 Born in the U.S. 65+ Female 1,810,015 4.58% 26,864,224 67.95%
13 Born in the U.S. 65+ Male 1,498,853 3.79% 28,363,077 71.74%
14 Born in Puerto Rico, Guam, the U.S. Virgin Islands, or the Northern Marianas 0 - 17 Female 1,654 0.00% 28,364,731 71.74%
15 Born in Puerto Rico, Guam, the U.S. Virgin Islands, or the Northern Marianas 0 - 17 Male 1,822 0.00% 28,366,553 71.75%
16 Born in Puerto Rico, Guam, the U.S. Virgin Islands, or the Northern Marianas 18 - 25 Female 3,145 0.01% 28,369,698 71.76%
17 Born in Puerto Rico, Guam, the U.S. Virgin Islands, or the Northern Marianas 18 - 25 Male 3,309 0.01% 28,373,007 71.76%
18 Born in Puerto Rico, Guam, the U.S. Virgin Islands, or the Northern Marianas 26 - 35 Female 4,663 0.01% 28,377,670 71.78%
19 Born in Puerto Rico, Guam, the U.S. Virgin Islands, or the Northern Marianas 26 - 35 Male 4,756 0.01% 28,382,426 71.79%
20 Born in Puerto Rico, Guam, the U.S. Virgin Islands, or the Northern Marianas 36 - 45 Female 5,279 0.01% 28,387,705 71.80%
21 Born in Puerto Rico, Guam, the U.S. Virgin Islands, or the Northern Marianas 36 - 45 Male 7,223 0.02% 28,394,928 71.82%
22 Born in Puerto Rico, Guam, the U.S. Virgin Islands, or the Northern Marianas 46 - 55 Female 6,145 0.02% 28,401,073 71.83%
23 Born in Puerto Rico, Guam, the U.S. Virgin Islands, or the Northern Marianas 46 - 55 Male 5,440 0.01% 28,406,513 71.85%
24 Born in Puerto Rico, Guam, the U.S. Virgin Islands, or the Northern Marianas 56 - 65 Female 7,705 0.02% 28,414,218 71.87%
25 Born in Puerto Rico, Guam, the U.S. Virgin Islands, or the Northern Marianas 56 - 65 Male 5,716 0.01% 28,419,934 71.88%
26 Born in Puerto Rico, Guam, the U.S. Virgin Islands, or the Northern Marianas 65+ Female 8,043 0.02% 28,427,977 71.90%
27 Born in Puerto Rico, Guam, the U.S. Virgin Islands, or the Northern Marianas 65+ Male 8,856 0.02% 28,436,833 71.93%
28 Born abroad of American parent(s) 0 - 17 Female 53,850 0.14% 28,490,683 72.06%
29 Born abroad of American parent(s) 0 - 17 Male 60,278 0.15% 28,550,961 72.21%
30 Born abroad of American parent(s) 18 - 25 Female 25,854 0.07% 28,576,815 72.28%
31 Born abroad of American parent(s) 18 - 25 Male 27,952 0.07% 28,604,767 72.35%
32 Born abroad of American parent(s) 26 - 35 Female 36,908 0.09% 28,641,675 72.44%
33 Born abroad of American parent(s) 26 - 35 Male 39,146 0.10% 28,680,821 72.54%
34 Born abroad of American parent(s) 36 - 45 Female 30,688 0.08% 28,711,509 72.62%
35 Born abroad of American parent(s) 36 - 45 Male 35,648 0.09% 28,747,157 72.71%
36 Born abroad of American parent(s) 46 - 55 Female 35,190 0.09% 28,782,347 72.80%
37 Born abroad of American parent(s) 46 - 55 Male 32,041 0.08% 28,814,388 72.88%
38 Born abroad of American parent(s) 56 - 65 Female 29,824 0.08% 28,844,212 72.96%
39 Born abroad of American parent(s) 56 - 65 Male 31,443 0.08% 28,875,655 73.04%
40 Born abroad of American parent(s) 65+ Female 21,979 0.06% 28,897,634 73.09%
41 Born abroad of American parent(s) 65+ Male 18,037 0.05% 28,915,671 73.14%
42 U.S. citizen by naturalization 0 - 17 Female 47,493 0.12% 28,963,164 73.26%
43 U.S. citizen by naturalization 0 - 17 Male 51,748 0.13% 29,014,912 73.39%
44 U.S. citizen by naturalization 18 - 25 Female 94,172 0.24% 29,109,084 73.63%
45 U.S. citizen by naturalization 18 - 25 Male 94,438 0.24% 29,203,522 73.86%
46 U.S. citizen by naturalization 26 - 35 Female 297,486 0.75% 29,501,008 74.62%
47 U.S. citizen by naturalization 26 - 35 Male 264,160 0.67% 29,765,168 75.28%
48 U.S. citizen by naturalization 36 - 45 Female 525,620 1.33% 30,290,788 76.61%
49 U.S. citizen by naturalization 36 - 45 Male 429,011 1.09% 30,719,799 77.70%
50 U.S. citizen by naturalization 46 - 55 Female 653,027 1.65% 31,372,826 79.35%
51 U.S. citizen by naturalization 46 - 55 Male 603,019 1.53% 31,975,845 80.88%
52 U.S. citizen by naturalization 56 - 65 Female 604,946 1.53% 32,580,791 82.41%
53 U.S. citizen by naturalization 56 - 65 Male 530,672 1.34% 33,111,463 83.75%
54 U.S. citizen by naturalization 65+ Female 756,574 1.91% 33,868,037 85.66%
55 U.S. citizen by naturalization 65+ Male 547,137 1.38% 34,415,174 87.05%
56 Not a citizen of the U.S. 0 - 17 Female 157,885 0.40% 34,573,059 87.45%
57 Not a citizen of the U.S. 0 - 17 Male 171,176 0.43% 34,744,235 87.88%
58 Not a citizen of the U.S. 18 - 25 Female 226,125 0.57% 34,970,360 88.45%
59 Not a citizen of the U.S. 18 - 25 Male 243,711 0.62% 35,214,071 89.07%
60 Not a citizen of the U.S. 26 - 35 Female 549,858 1.39% 35,763,929 90.46%
61 Not a citizen of the U.S. 26 - 35 Male 585,123 1.48% 36,349,052 91.94%
62 Not a citizen of the U.S. 36 - 45 Female 609,386 1.54% 36,958,438 93.48%
63 Not a citizen of the U.S. 36 - 45 Male 636,381 1.61% 37,594,819 95.09%
64 Not a citizen of the U.S. 46 - 55 Female 476,933 1.21% 38,071,752 96.29%
65 Not a citizen of the U.S. 46 - 55 Male 486,775 1.23% 38,558,527 97.53%
66 Not a citizen of the U.S. 56 - 65 Female 290,869 0.74% 38,849,396 98.26%
67 Not a citizen of the U.S. 56 - 65 Male 263,811 0.67% 39,113,207 98.93%
68 Not a citizen of the U.S. 65+ Female 244,246 0.62% 39,357,453 99.55%
69 Not a citizen of the U.S. 65+ Male 179,200 0.45% 39,536,653 100.00%

O si quiero solamente aquellas personas que no nacieron en estados unidos

mask_noncitizen = data['CIT'] == 'Not a citizen of the U.S.'
data[mask_noncitizen].stb.freq(['CIT', 'AGE_BIN', 'SEX'], value = 'PWGTP', style = True, sort_cols=True)
CIT AGE_BIN SEX PWGTP Percent Cumulative PWGTP Cumulative Percent
56 Not a citizen of the U.S. 0 - 17 Female 157,885 3.08% 157,885 3.08%
57 Not a citizen of the U.S. 0 - 17 Male 171,176 3.34% 329,061 6.43%
58 Not a citizen of the U.S. 18 - 25 Female 226,125 4.42% 555,186 10.84%
59 Not a citizen of the U.S. 18 - 25 Male 243,711 4.76% 798,897 15.60%
60 Not a citizen of the U.S. 26 - 35 Female 549,858 10.74% 1,348,755 26.34%
61 Not a citizen of the U.S. 26 - 35 Male 585,123 11.42% 1,933,878 37.76%
62 Not a citizen of the U.S. 36 - 45 Female 609,386 11.90% 2,543,264 49.66%
63 Not a citizen of the U.S. 36 - 45 Male 636,381 12.43% 3,179,645 62.08%
64 Not a citizen of the U.S. 46 - 55 Female 476,933 9.31% 3,656,578 71.40%
65 Not a citizen of the U.S. 46 - 55 Male 486,775 9.50% 4,143,353 80.90%
66 Not a citizen of the U.S. 56 - 65 Female 290,869 5.68% 4,434,222 86.58%
67 Not a citizen of the U.S. 56 - 65 Male 263,811 5.15% 4,698,033 91.73%
68 Not a citizen of the U.S. 65+ Female 244,246 4.77% 4,942,279 96.50%
69 Not a citizen of the U.S. 65+ Male 179,200 3.50% 5,121,479 100.00%

¡Una visualización rapidín!

import altair as alt

# nota que le no estoy usando style = True
vis_data = data[mask_noncitizen].stb.freq(['CIT', 'AGE_BIN', 'SEX'], value = 'PWGTP', sort_cols=True)

alt.Chart(vis_data).mark_bar().encode(
    x='PWGTP:Q',
    y='SEX:O',
    color='SEX:N',
    row='AGE_BIN:O'
).properties(
    title = "No. de personas no nacidas en EEUU en California (ACS 2017)"
)

En conclusión, sidetable es una herramienta sencilla, fácil de usar y muy útil.

Créditos: Aprendí de sidetable en el podcast python bytes en el episodio #186 pero mientras escribía esta nota encontré este blog en twitter que también esta explorando sidetable pero en inglés: https://beta.deepnote.com/article/sidetable-pandas-methods-you-didnt-know-you-needed así que gracias a ambos recursos 🤓


¿Qué te pareció la nota? Mandanos un tuit a @tacosdedatos o envianos un correo a ✉️ sugerencias@tacosdedatos.com. Y recuerda que puedes subscribirte a nuestro boletín semanal aquí debajo.