Este tutorial adapta pedagógicamente el Día 76, que abre la segunda parte del bloque de ciencia de datos (Días 76 a 81). Las secciones 📝 Contenido original del curso provienen de las lecciones 559 a 567; el cuaderno de inicio y el archivo
apps.csvestán en los recursos del curso (Google Play Store Project (Start).zip). En varias lecciones parte del código aparece solo en imágenes; aquí se reconstruye y se verifica. Los retos 🟢🟡🟠🔴, las secciones sobre cómo leer un diagrama de caja y la escala logarítmica, el proyecto extra y la etapa 1 del Laboratorio de Datos Abiertos (nuevo proyecto integrador de los Días 76 a 81) son creaciones de esta adaptación. Las correcciones al material original aparecen como 🛠️ Nota técnica.
Nota sobre las verificaciones: el 27 de septiembre de 2026, con Python 3.14, pandas 3.0.6 y plotly 7.1.0, y con una copia pública de
apps.csv(publicada en GitHub por otro estudiante del mismo curso), se ejecutó todo el código. Los resultados coinciden con el curso: 10 841 filas y 12 columnas; 9 367 filas sin faltantes; 476 duplicados; 8 199 aplicaciones únicas; 33 categorías; 53 géneros; mediana de instalaciones de 500 000 (gratuitas) contra 5 000 (pagas); precio mediano de 2,99 dólares. Hay tres diferencias menores: el reto pide eliminarAndroid_Version, pero la columna se llamaAndroid_Ver(KeyError); en pandas 3 las columnas de texto ya no son de tipoobjectsinostr; y entre las 5 aplicaciones más caras hay 3 médicas (no 4), porque dos aplicaciones empatan a 29,99 dólares y el orden del empate puede cambiar.
Tabla de contenidos
- Antes de empezar: ¿vale la pena crear una app?
- Explorar y limpiar: faltantes y duplicados
- Primeras preguntas: calificación, tamaño y reseñas
- Plotly: gráficos circulares y de dona
- De texto a número: instalaciones y precios
- Barras y dispersión: categorías competidas y populares
- Datos anidados:
splitystack - Barras agrupadas y diagramas de caja
- 📖 Leer un diagrama de caja y la escala logarítmica
- 🚀 Proyecto del día: informe para una desarrolladora de apps
- 🌟 Proyecto extra: las apps de nuestro curso
- 🏗️ Laboratorio de Datos Abiertos — Etapa 1
- 🧩 Comprueba lo aprendido
- 🤖 Programar con apoyo de IA
- 🛠️ Correcciones técnicas al material original
- 📋 Matriz de trazabilidad
- 📦 Evidencias de aprendizaje del Día 76
0. Antes de empezar: ¿vale la pena crear una app?
(Fuente: 559-day-76-goals-what-you-will-make-by-the-end-of-the-day.md)
📝 Contenido original del curso
¿Alguna vez pensaste en crear tu propia app para iOS o Android? Hoy replicaremos parte de la analítica que ofrecen empresas como App Annie o Sensor Tower, que orienta las estrategias de desarrollo y mercadeo de muchas compañías. Compararemos miles de apps de Google Play para averiguar:
- qué tan competidas son las categorías (juegos, estilo de vida, clima…);
- qué categoría ofrece oportunidades por su popularidad;
- cuántas descargas se pierden al cobrar por una app;
- cuánto se puede cobrar razonablemente;
- qué apps pagas han tenido más ingresos;
- cuántas apps pagas recuperan su costo de desarrollo.
Aprenderás a eliminar duplicados, a quitar símbolos y convertir datos a formato numérico, a manejar columnas con datos anidados y a crear con plotly barras verticales, horizontales y agrupadas, gráficos circulares y de dona, y gráficos de dispersión con escalas de color.
Preparación: descarga el ZIP, sube el cuaderno a Google Drive, ábrelo con Google Colaboratory y agrega el CSV.
Prerequisitos: exploración y limpieza con pandas (Día 72),
groupby,aggymerge(Día 74). Nuevo hoy: la bibliotecaplotly. En Colab ya viene instalada; en tu computador:pip install plotly.
🧠 ¿Qué aprenderemos?
| Pregunta de negocio | Herramienta del día |
|---|---|
| ¿Los datos son confiables? | .sample(), .duplicated(), .drop_duplicates(subset=...) |
| ¿Cuánto cuesta y cuántas descargas tiene? | .str.replace() + pd.to_numeric() |
| ¿Qué categorías son más competidas o populares? | px.bar, px.scatter con escala logarítmica |
| ¿Cuántos géneros hay de verdad? | .str.split(expand=True).stack() |
| ¿Cuánto pierdo si cobro? | px.box con color y notched |
1. Explorar y limpiar: faltantes y duplicados
(Fuente: 560-data-cleaning-removing-nan-values-and-duplicates.md)
🎯 Objetivo de aprendizaje
Detectar y eliminar filas faltantes y duplicadas, decidiendo con qué columnas se identifica un duplicado.
📝 Contenido original del curso
Reto: ¿cuántas filas y columnas tiene df_apps? ¿Qué columnas? Mira una muestra aleatoria de 5 filas con .sample() (se abre en otra pestaña).
Solución: df_apps.shape da 10 841 filas y 12 columnas. Ya se ven problemas: hay NaN en Rating y Type, y la columna Price tiene signos $. .sample(n) devuelve n filas al azar: otra forma práctica de inspeccionar.
Reto: elimina las columnas Last_Updated y Android_Version. Reto: ¿cuántas filas tienen NaN en Rating? Crea df_apps_clean sin esas filas.
Solución: se pasa la lista de columnas a .drop() con axis=1. Al filtrar las filas con .isna() se ve que los NaN de las calificaciones van con apps sin reseñas (y sin instalaciones): tiene sentido.
df_apps.drop(['Last_Updated', 'Android_Ver'], axis=1, inplace=True)
df_apps_clean = df_apps.dropna()
df_apps_clean.shape # (9367, 10)Reto: ¿hay duplicados? Usa .duplicated() (se abre en otra pestaña). ¿Cuántas entradas hay para "Instagram"? Elimina los duplicados con .drop_duplicates().
duplicated_rows = df_apps_clean[df_apps_clean.duplicated()]
print(duplicated_rows.shape) # (476, 10)¿Basta con .drop_duplicates() sin más? No: quedan 3 copias de Instagram, porque tienen un número de reseñas distinto. Hay que indicar qué columnas se comparan para identificar duplicados. Así quedan 8 199 entradas.
Lo que conviene saber de los datos:
- Es una muestra: no incluye todas las apps de Android (hay millones).
- Se supone representativa, aunque no necesariamente lo es: el web scraping recibió resultados según la ubicación y el comportamiento de quien la recopiló (Lavanya Gupta).
- Se recopiló hacia 2017/2018; los precios están en dólares de ese momento.
- El tamaño se convirtió a megabytes (
Size_MBs); si faltaba, se usó el promedio de la categoría. - Las instalaciones no son exactas: si una app tiene 245 239 instalaciones, Google muestra "100,000+". Se quitó el "+" y, por simplicidad, se toma ese número como exacto.
📖 Explicación conceptual (verificada)
La línea que el curso muestra solo en imagen es, verificado con los datos:
df_apps_clean = df_apps_clean.drop_duplicates(subset=['App', 'Type', 'Price'])
df_apps_clean.shape # (8199, 10)
(df_apps_clean.App == 'Instagram').sum() # 1¿Qué es un duplicado? Depende de la pregunta. Para pandas, por defecto, dos filas son duplicadas solo si todas sus columnas son iguales. Las 4 filas de Instagram difieren en Reviews (66 577 313, 66 577 446, 66 509 917…): el scraping las capturó en momentos distintos. Para nosotros son la misma app, así que comparamos solo nombre, tipo y precio.
| Opción | Filas resultantes | ¿Instagram? |
|---|---|---|
dropna() |
9 367 | 4 filas |
drop_duplicates() |
8 891 | 3 filas |
drop_duplicates(subset=['App', 'Type', 'Price']) |
8 199 | 1 fila |
🛠️ Nota técnica (verificado): el enunciado dice
Android_Version, pero la columna del CSV se llamaAndroid_Ver; con el nombre del enunciado apareceKeyError: "['Android_Version'] not found in axis". La solución del curso ya usa el nombre correcto.
🛠️ Nota técnica (verificado): de las 1 474 filas sin calificación, 596 tienen 0 reseñas y el 85 % tiene 10 o menos, pero alguna llega a 3 248. La explicación del curso vale para la mayoría, no para todas. Además,
dropna()elimina las filas con cualquier faltante: aquí coincide con quitar los NaN deRating, porque la única fila sinTypetampoco tiene calificación. Si quieres ser preciso con lo que pide el reto:df_apps.dropna(subset=['Rating']).
⚠️ Errores frecuentes
| Error | Qué ocurre | Cómo solucionarlo |
|---|---|---|
Usar drop_duplicates() sin subset |
Quedan "casi duplicados" (Instagram 3 veces) | Decide qué columnas definen "la misma app" |
Olvidar axis=1 en drop |
pandas busca filas con ese nombre y lanza KeyError |
drop(columns=[...]) es más legible |
Contar duplicados con duplicated() y creer que son todas las copias |
duplicated() marca las copias después de la primera |
Usa keep=False para ver todas |
| Borrar sin mirar | Pierdes datos válidos | Primero mira las filas (df[df.duplicated()]), luego borra |
Retos adicionales (creados para esta adaptación)
- 🟢 Reto 1: muestra todas las copias de las apps duplicadas con
duplicated(subset=['App'], keep=False). ¿Qué app aparece más veces? - 🟡 Reto 2: ¿hay apps con el mismo nombre pero distinta categoría? Encuéntralas. ¿Son la misma app o apps distintas que se llaman igual?
- 🟠 Reto 3: en lugar de quedarte con la primera copia, quédate con la que tenga más reseñas (pista: ordena por
Reviewsantes dedrop_duplicates). ¿Cambia el número de filas? ¿Cambian los datos?
2. Primeras preguntas: calificación, tamaño y reseñas
(Fuente: 561-preliminary-exploration-the-highest-ratings-most-reviews-and-largest-size.md)
📝 Contenido original del curso
Retos: ¿qué apps tienen la mejor calificación? ¿Qué problema hay si solo miras la calificación? ¿Cuál es el tamaño de las apps más grandes? ¿Hay un límite? ¿Qué apps tienen más reseñas? ¿Hay apps pagas entre las 50 primeras?
df_apps_clean.sort_values('Rating', ascending=False).head()
df_apps_clean.sort_values('Size_MBs', ascending=False).head()
df_apps_clean.sort_values('Reviews', ascending=False).head(50)- Solo las apps con muy pocas reseñas (y pocas instalaciones) tienen 5 estrellas perfectas, "probablemente de amigos y familiares".
- Parece haber un límite de 100 MB, impuesto por la propia tienda; varias apps lo alcanzan exactamente.
- Entre las 50 apps con más reseñas están Facebook, WhatsApp, Instagram… y ninguna es paga.
📖 Resultados verificados y explicación
- La primera app con 5,0 estrellas (KBA-EZ Health Guide) tiene 4 reseñas y 1 instalación. Un promedio de pocas opiniones es poco confiable: una sola opinión lo cambia mucho. Es la misma idea que un 5,0 en una materia con una sola nota.
- 14 apps pesan exactamente 100 MB, y ninguna más. Cuando muchos datos se "amontonan" justo en un valor máximo, suele haber un límite externo (aquí, de la tienda en esa época).
- 0 apps pagas entre las 50 más reseñadas (verificado).
💡 Límites que cambian: el límite de 100 MB era el de la tienda en 2018. Las reglas actuales de Google Play son otras; si lo necesitas, consulta la documentación oficial para desarrolladores (Play Console) en lugar de suponer que sigue igual.
🔍 Antes de programar
¿Cómo definirías "la mejor app" de forma más justa que solo con la calificación? ¿Qué pasaría si exigieras un mínimo de reseñas? ¿Qué mínimo elegirías y por qué?
Retos adicionales (creados para esta adaptación)
- 🟢 Reto 1: muestra las 10 apps con mejor calificación entre las que tienen al menos 10 000 reseñas.
- 🟡 Reto 2: calcula el promedio de calificación de las apps gratuitas y de las pagas. ¿Te sorprende? (Verificado: 4,17 y 4,27.) ¿Por qué no basta ese número para decir que las pagas son "mejores"?
- 🔴 Reto 3 — Pensamiento computacional: diseña tu propio "puntaje de confianza" que combine calificación y número de reseñas. Escríbelo como función, pruébalo con 3 apps y explica por qué tu fórmula es razonable.
3. Plotly: gráficos circulares y de dona
(Fuente: 562-data-visualisation-with-plotly-create-pie-and-donut-charts.md)
🎯 Objetivo de aprendizaje
Crear un gráfico interactivo con plotly.express y personalizarlo con parámetros y con update_traces.
📝 Contenido original del curso
Todas las apps tienen una clasificación de contenido ("Everyone", "Teen", "Mature 17+"…). La contamos con .value_counts() y la visualizamos con plotly (se abre en otra pestaña), una biblioteca popular que se puede usar junto con Matplotlib o en su lugar.
import plotly.express as px
ratings = df_apps_clean.Content_Rating.value_counts()
fig = px.pie(labels=ratings.index,
values=ratings.values,
title="Content Rating",
names=ratings.index)
fig.update_traces(textposition='outside', textinfo='percent+label')
fig.show()plotly.express es la forma más rápida de crear un gráfico atractivo con poco código. Para configurar algo que no aparece entre los parámetros de la función se usa .update_traces(): en el lenguaje de plotly, las trazas (traces) son las marcas gráficas de la figura, colecciones de atributos. Para una dona basta el argumento hole:
fig = px.pie(labels=ratings.index, values=ratings.values,
title="Content Rating", names=ratings.index, hole=0.6)
fig.update_traces(textposition='inside', textfont_size=15, textinfo='percent')
fig.show()📖 Explicación conceptual (adaptación)
Matplotlib contra plotly:
| Matplotlib | plotly | |
|---|---|---|
| Resultado | Imagen fija (PNG) | Gráfico interactivo (HTML): pasar el ratón, acercar, ocultar series |
| Estilo | Tú configuras casi todo | Buen aspecto por defecto |
| Ideal para | Informes impresos, control total | Explorar datos, páginas web, tableros |
| Guardar | plt.savefig('g.png') |
fig.write_html('g.html') (o write_image, que requiere el paquete kaleido) |
Tres niveles de configuración en plotly:
- Parámetros de la función (
title,names,hole,color…). fig.update_traces(...): cambia las marcas (texto, tamaño, bordes).fig.update_layout(...): cambia el "marco" (títulos de ejes, escalas, leyenda).
¿Cuándo usar un gráfico circular? Solo para mostrar partes de un todo con pocas categorías (idealmente 5 o menos). Aquí hay 6 clasificaciones y dos tienen 3 y 1 apps: sus porciones son invisibles. Para comparar categorías con precisión, las barras son mejores, porque el ojo compara longitudes mejor que ángulos.
🔎 Ejemplo verificado
| Clasificación | Apps |
|---|---|
| Everyone | 6 621 |
| Teen | 912 |
| Mature 17+ | 357 |
| Everyone 10+ | 305 |
| Adults only 18+ | 3 |
| Unrated | 1 |
"Everyone" es el 80,8 %: el gráfico circular lo muestra muy bien. Pero ¿podrías leer en el círculo la diferencia entre "Adults only 18+" y "Unrated"? Por eso un gráfico circular responde bien a "¿qué parte es la mayoría?" y mal a "¿cuánto más grande es A que B?".
⚠️ Errores frecuentes
holefuera de rango: debe estar entre 0 y 1;hole=1.2produceValueError.- Olvidar
fig.show()fuera de un cuaderno: el gráfico se crea pero no se ve. - Pasar
valuessinnames: las porciones quedan sin nombre en la leyenda.
Retos adicionales (creados para esta adaptación)
- 🟢 Reto 1: crea la dona de apps gratuitas contra pagas (
Type). - 🟡 Reto 2: agrupa "Adults only 18+" y "Unrated" en una categoría "Otras" antes de graficar. ¿Mejora la lectura?
- 🟠 Reto 3: crea la misma información como barras horizontales y compara: ¿cuál responde mejor a "¿cuántas apps Teen más que Mature hay?"?
4. De texto a número: instalaciones y precios
(Fuente: 563-numeric-type-conversions-for-the-installations-y-price-data.md)
🎯 Objetivo de aprendizaje
Limpiar columnas numéricas guardadas como texto, detectar datos basura y crear una columna calculada.
📝 Contenido original del curso
Reto: ¿cuántas apps tienen más de mil millones de instalaciones? ¿Cuántas solo una? 1) Revisa el tipo de dato de Installs. 2) Cuenta las apps de cada nivel. 3) Convierte a número (en dos pasos: primero quita los caracteres no numéricos).
.describe() o .info() muestran que la columna no es numérica; por eso, al agrupar, el orden no sirve: Python no reconoce los números por las comas.
df_apps_clean.Installs = df_apps_clean.Installs.astype(str).str.replace(',', "")
df_apps_clean.Installs = pd.to_numeric(df_apps_clean.Installs)
df_apps_clean[['App', 'Installs']].groupby('Installs').count()Reto: convierte el precio a número e investiga las 20 apps más caras. Elimina las que cuestan más de 250 dólares. Agrega Revenue_Estimate = precio × instalaciones. ¿Cuáles son las 10 apps pagas con más ingresos? ¿Cuántas son juegos?
df_apps_clean.Price = df_apps_clean.Price.astype(str).str.replace('$', "")
df_apps_clean.Price = pd.to_numeric(df_apps_clean.Price)
df_apps_clean.sort_values('Price', ascending=False).head(20)Hay 15 apps "I am Rich", todas de 300 dólares o más. En 2008, Armin Heinrich publicó la primera en la tienda de iOS por 999,90 dólares: no hace nada, solo muestra una gema "para probar a tus amigos lo rico que eres". Vendió 7 copias antes de que Apple la retirara, e inspiró copias en Android. Las altas instalaciones probablemente se "fabricaron" ofreciéndolas gratis por un tiempo. Dejar estos datos basura distorsionaría el análisis:
df_apps_clean = df_apps_clean[df_apps_clean['Price'] < 250]
df_apps_clean['Revenue_Estimate'] = df_apps_clean.Installs.mul(df_apps_clean.Price)
df_apps_clean.sort_values('Revenue_Estimate', ascending=False)[:10]La estimación supone, generosamente, que todas las instalaciones se pagaron al precio listado (hay promociones y regalos). Gana Minecraft, con cerca de 70 millones de dólares. Minecraft, Bloons y Card Wars están en la categoría Family, no en Game: contándolos, 7 de las 10 son juegos. Google Play es bastante flexible con sus etiquetas.
📖 Explicación conceptual (verificada)
Los dos pasos de la conversión:
"1,000,000" --(.str.replace(',', ''))--> "1000000" --(pd.to_numeric)--> 1000000
"$4.99" --(.str.replace('$', ''))--> "4.99" --(pd.to_numeric)--> 4.99Si intentas pd.to_numeric directamente sobre "1,000,000", obtienes ValueError: Unable to parse string "1,000,000". El error te dice exactamente qué valor falló: léelo.
Resultados verificados: 20 apps con 1 000 000 000 de instalaciones y 3 con 1; 15 apps de 250 dólares o más (14 "I am Rich" y "most expensive app (H)"); tras el filtro quedan 8 184 apps.
| # | App | Categoría | Ingreso estimado (USD) |
|---|---|---|---|
| 1 | Minecraft | FAMILY | 69 900 000 |
| 2 | Hitman Sniper | GAME | 9 900 000 |
| 3 | Grand Theft Auto: San Andreas | GAME | 6 990 000 |
| 4 | Facetune - For Free | PHOTOGRAPHY | 5 990 000 |
| 5 | Sleep as Android Unlock | LIFESTYLE | 5 990 000 |
🛠️ Nota técnica (verificado): en pandas 3, las columnas de texto aparecen como tipo
str(antesobject). El diagnóstico es el mismo: "no es numérica". Y.str.replace('$', '')trata el$literalmente porque desde pandas 2.0regex=Falsees el valor por defecto; en versiones antiguas$era un símbolo especial de las expresiones regulares ("fin del texto"). Escribirlo explícito evita sorpresas:.str.replace('$', '', regex=False).
🛠️ Nota técnica (verificado): el curso dice que 4 de las 5 apps más caras son médicas. Con los datos verificados son 3 (Vargo Anesthesia Mega App, LTC AS Legal, A Manual of Acupuncture), más "I am Rich Person" y "AP Art History Flashcards". El quinto puesto es un empate a 29,99 dólares con "EMT PASS" (médica), así que el resultado depende del orden en que
sort_valuesdeje los empates.
El filtro no atrapó toda la basura: "I am Rich Person" (37,99 dólares) e "I am Rich Premium Plus" (18,99) siguen en los datos. Un umbral numérico es una regla aproximada: siempre revisa qué quedó.
⚠️ Errores frecuentes
| Error | Síntoma | Solución |
|---|---|---|
| Convertir sin quitar las comas | ValueError: Unable to parse string |
Primero .str.replace, luego pd.to_numeric |
| Olvidar asignar el resultado | La columna sigue siendo texto | df.col = df.col.str.replace(...) |
| Ordenar números guardados como texto | "5" aparece después de "1,000" | Convierte antes de ordenar o agrupar |
Filtrar con <= 250 en vez de < 250 |
Cambia qué filas se incluyen en el borde | Decide el borde y documéntalo |
Retos adicionales (creados para esta adaptación)
- 🟢 Reto 1: ¿qué porcentaje de las apps tiene menos de 1 000 instalaciones?
- 🟡 Reto 2: busca con
.str.contains('rich', case=False)las apps que siguen pareciendo "basura" tras el filtro. Decide y justifica una regla mejor. - 🟠 Reto 3: calcula el ingreso estimado por categoría (suma) solo para apps pagas. ¿Qué categoría gana? ¿Cambia si excluyes Minecraft?
- 🔴 Reto 4 — Pensamiento computacional: las instalaciones son "órdenes de magnitud" (1 000+, 5 000+…). Si una app dice 1 000 000, en realidad tiene entre 1 000 000 y 4 999 999. Calcula para Minecraft el ingreso mínimo y máximo posibles y explica qué tan confiable es el "70 millones".
5. Barras y dispersión: categorías competidas y populares
(Fuente: 564-plotly-bar-charts-y-scatter-plots-the-most-competitive-y-popular-app-categories.md)
🎯 Objetivo de aprendizaje
Distinguir competencia (cuántas apps) de popularidad (cuántas descargas) y combinar ambas en un gráfico de dispersión.
📝 Contenido original del curso
Si lanzaras una app, ¿apuntarías a una categoría con muchas apps, a una con muchas descargas o a una popular donde las descargas estén repartidas? Hay 33 categorías (nunique()). Las 10 con más apps:
top10_category = df_apps_clean.Category.value_counts()[:10]
bar = px.bar(x=top10_category.index, # index = nombre de la categoría
y=top10_category.values)
bar.show()Family y Game son las más competidas: será difícil destacar allí. Pero importa también cuántas descargas tiene cada categoría:
category_installs = df_apps_clean.groupby('Category').agg({'Installs': pd.Series.sum})
category_installs.sort_values('Installs', ascending=True, inplace=True)
h_bar = px.bar(x=category_installs.Installs,
y=category_installs.index,
orientation='h',
title='Category Popularity')
h_bar.update_layout(xaxis_title='Number of Downloads', yaxis_title='Category')
h_bar.show()Game y Tools son las más populares. Reto: un gráfico de dispersión con el número de apps y de instalaciones por categoría (pistas: size, hover_name, color, y yaxis=dict(type='log')).
cat_number = df_apps_clean.groupby('Category').agg({'App': pd.Series.count})
cat_merged_df = pd.merge(cat_number, category_installs, on='Category', how="inner")
scatter = px.scatter(cat_merged_df, x='App', y='Installs',
title='Category Concentration',
size='App', hover_name=cat_merged_df.index, color='Installs')
scatter.update_layout(xaxis_title="Number of Apps (Lower=More Concentrated)",
yaxis_title="Installs", yaxis=dict(type='log'))
scatter.show()Family, Tools y Game tienen muchas apps que comparten muchas descargas; en cambio, en Video Players y Entertainment las descargas se concentran en pocas apps.
📖 Explicación conceptual (verificada)
Un gráfico, tres variables: posición x (apps), posición y (instalaciones) y tamaño/color. Pasar el DataFrame completo y los nombres de columnas (x='App') es más limpio que pasar series sueltas, y permite hover_name para identificar cada punto.
Una medida más directa de concentración (adaptación): instalaciones por app.
| Categoría | Apps | Instalaciones por app (promedio) |
|---|---|---|
| COMMUNICATION | 257 | ≈ 43 millones |
| SOCIAL | 203 | ≈ 27 millones |
| VIDEO_PLAYERS | 148 | ≈ 26 millones |
| ENTERTAINMENT | 102 | ≈ 21 millones |
Cuidado: un promedio alto puede deberse a una sola app gigante (WhatsApp en Communication). Eso es exactamente lo que significa "concentrada".
💡
pd.Series.sumo'sum':agg({'Installs': pd.Series.sum})funciona, pero la forma habitual y más corta esagg({'Installs': 'sum'})ogroupby('Category').Installs.sum().
⚠️ Errores frecuentes
- Ordenar al revés en barras horizontales: plotly dibuja la primera fila abajo; por eso el curso ordena de forma ascendente para que la mayor quede arriba.
mergesin columna común: aquíCategoryes el índice de ambos resultados;on='Category'funciona porque pandas acepta nombres de índice. Si fallara, usaleft_index=True, right_index=True.- Escala lineal con datos muy dispares: las categorías pequeñas quedan aplastadas contra el eje (sección 8).
Retos adicionales (creados para esta adaptación)
- 🟢 Reto 1: cambia la escala de color con
color_continuous_scale='Viridis'. - 🟡 Reto 2: calcula para cada categoría qué porcentaje de sus instalaciones corresponde a su app número 1. ¿Cuál es la más concentrada?
- 🔴 Reto 3 — Pensamiento computacional: propón con datos una categoría "prometedora" para una app nueva (popular pero no concentrada). Define tu criterio, aplícalo con código y defiéndelo en 5 líneas.
6. Datos anidados: split y stack
(Fuente: 565-extracting-nested-column-data-using-stack.md)
🎯 Objetivo de aprendizaje
Separar una columna con varios valores en una celda para poder contarlos.
📝 Contenido original del curso
Reto: ¿cuántos géneros hay? ¿Una app puede tener varios? ¿Qué pasa al usar .value_counts() en una columna anidada? Usa .split() y .stack() (se abre en otra pestaña).
Genres tiene 114 valores únicos, pero la cifra engaña: el punto y coma (;) separa varios géneros en una misma celda.
# Separar por punto y coma y apilar
stack = df_apps_clean.Genres.str.split(';', expand=True).stack()
print(f'We now have a single column with shape: {stack.shape}')
num_genres = stack.value_counts()
print(f'Number of genres: {len(num_genres)}') # 53Reto: el gráfico de los 15 géneros principales con escalas de color (color_continuous_scale) y sin eje de color (coloraxis_showscale):
bar = px.bar(x=num_genres.index[:15], y=num_genres.values[:15],
title='Top Genres', hover_name=num_genres.index[:15],
color=num_genres.values[:15], color_continuous_scale='Agsunset')
bar.update_layout(xaxis_title='Genre', yaxis_title='Number of Apps',
coloraxis_showscale=False)
bar.show()📖 Explicación conceptual (verificada)
Genres split(expand=True) stack()
"Puzzle;Brain Games" --> | Puzzle | Brain Games | --> Puzzle
"Tools" --> | Tools | None | --> Brain Games
Tools.str.split(';', expand=True)crea una columna por género (las celdas sobrantes quedan vacías)..stack()"apila" todas las columnas en una sola serie y descarta los vacíos.
Resultado verificado: una serie de 16 368 géneros, con 53 géneros distintos (Tools 719, Education 587, Entertainment 498…).
Alternativa moderna: explode() hace lo mismo con una lista por celda, y a muchos les resulta más legible:
df_apps_clean.Genres.str.split(';').explode().value_counts() # también 53 géneros¿Por qué importa? Guardar varios valores en una celda es un error de diseño común (en hojas de cálculo, formularios de Google con casillas múltiples…). Separarlos es un paso clásico de limpieza: lo usarás en el proyecto extra.
⚠️ Errores frecuentes
- Contar sin separar:
nunique()= 114 cuenta combinaciones, no géneros. - Espacios invisibles: si el separador fuera
"; "(con espacio), "Puzzle" y " Puzzle" serían distintos. Solución:.str.strip()después de separar. - Separador equivocado: con
split(',')no se separa nada y el conteo queda igual.
Retos adicionales (creados para esta adaptación)
- 🟢 Reto 1: ¿cuántas apps tienen más de un género? (Pista:
.str.contains(';').) - 🟡 Reto 2: ¿qué pareja de géneros aparece junta más veces?
- 🟠 Reto 3: repite el conteo con
explode()y comprueba conassertque da el mismo resultado questack().
7. Barras agrupadas y diagramas de caja
(Fuente: 566-grouped-bar-charts-and-box-plots-with-plotly.md y 567-learning-points-y-summary.md)
🎯 Objetivo de aprendizaje
Comparar grupos con barras agrupadas y distribuciones con diagramas de caja, y usar esos gráficos para responder preguntas de negocio.
📝 Contenido original del curso
La mayoría de las apps son gratuitas (Type.value_counts()), pero quizá algunas categorías tengan más pagas. Agrupamos por categoría y tipo; con as_index=False los grupos quedan como columnas:
df_free_vs_paid = df_apps_clean.groupby(["Category", "Type"],
as_index=False).agg({'App': pd.Series.count})Reto: el gráfico de barras agrupadas, ordenado con categoryorder = 'total descending':
g_bar = px.bar(df_free_vs_paid, x='Category', y='App',
title='Free vs Paid Apps by Category',
color='Type', barmode='group')
g_bar.update_layout(xaxis_title='Category', yaxis_title='Number of Apps',
xaxis={'categoryorder': 'total descending'},
yaxis=dict(type='log'))
g_bar.show()Hay pocas apps pagas, pero algunas categorías tienen relativamente más: Personalization, Medical y Weather. Según la categoría, podría tener sentido una app paga. Nuevas preguntas: ¿cuánto cobrar?, ¿cuánto ganar?, ¿cuántas descargas se pierden?
Los diagramas de caja muestran estadísticas descriptivas: mediana, máximo, mínimo y cuartiles. Reto: instalaciones de apps gratuitas contra pagas.
box = px.box(df_apps_clean, y='Installs', x='Type', color='Type',
notched=True, points='all',
title='How Many Downloads are Paid Apps Giving Up?')
box.update_layout(yaxis=dict(type='log'))
box.show()La mediana de descargas es 500 000 para las gratuitas y cerca de 5 000 para las pagas: muchísimo menor.
Reto: ingresos estimados de las apps pagas por categoría (orden 'min ascending'). Si una app cuesta unos 30 000 dólares, ¿la app de fotografía promedio recupera su costo?
df_paid_apps = df_apps_clean[df_apps_clean['Type'] == 'Paid']
box = px.box(df_paid_apps, x='Category', y='Revenue_Estimate',
title='How Much Can Paid Apps Earn?')
box.update_layout(xaxis_title='Category', yaxis_title='Paid App Ballpark Revenue',
xaxis={'categoryorder': 'min ascending'}, yaxis=dict(type='log'))
box.show()En muy pocas categorías la app mediana cubriría ese costo; la app paga de fotografía mediana ganó unos 20 000 dólares. Muchas necesitarían publicidad o compras dentro de la app. Algunas categorías tienen muchos valores atípicos con ingresos mucho mayores (Medical, Personalization, Tools, Game, Family).
Reto: el precio mediano de una app paga y la comparación de precios por categoría ('max descending'). El precio mediano es 2,99 dólares; Medical tiene las apps más caras y una mediana de 5,49; Personalization es barata (1,49); Business (4,99) y Dating (6,99) también cobran más.
Resumen del día: .sample(); .duplicated() y .drop_duplicates(); pd.to_numeric(); con plotly, gráficos circulares, de dona, de barras, de caja y de dispersión.
📖 Resultados verificados
| Afirmación del curso | Verificado |
|---|---|
| Personalization, Medical y Weather con más apps pagas en proporción | 21,8 %, 21,6 % y 9,7 % de apps pagas (las tres primeras) |
| Mediana de instalaciones: 500 000 contra ≈ 5 000 | 500 000 y 5 000 |
| Mediana de ingresos de fotografía ≈ 20 000 | 19 990 dólares |
| Precio mediano 2,99; Medical 5,49; Personalization 1,49; Business 4,99; Dating 6,99 | Todos coinciden |
¿Cuántas apps pagas cubren 30 000 dólares? Solo el 34,8 % de las 589 apps pagas, y en 8 de las 28 categorías con apps pagas la mediana llega a esa cifra. Además, estos son ingresos brutos estimados: la tienda se queda con una comisión y el cálculo supone que todas las instalaciones se pagaron. La cifra de 30 000 dólares es una referencia del curso, no un dato de este conjunto.
⚠️ Errores frecuentes
- Confundir
barmode='group'con'stack':grouppone las barras lado a lado;stack, una encima de otra (por defecto enpx.barconcolor). categoryordermal escrito: un valor inválido lanzaValueErrorcon la lista de valores permitidos: léela.- Olvidar la escala logarítmica: con valores de 1 a mil millones, la caja de las pagas se vuelve una raya pegada al cero.
8. 📖 Leer un diagrama de caja y la escala logarítmica
(Sección creada para esta adaptación.)
El diagrama de caja
o <- valor atípico (outlier): más allá de 1,5 × RIC
|
───── <- bigote superior (hasta el último dato no atípico)
│ │
│───│ <- Q3: el 75 % de los datos está por debajo
│ │
│═══│ <- MEDIANA (Q2): la mitad está por debajo y la mitad por encima
│ │
│───│ <- Q1: el 25 % está por debajo
───── <- bigote inferior- RIC (rango intercuartílico) = Q3 − Q1: la altura de la caja, donde está la mitad central de los datos.
- La línea del medio es la mediana, no el promedio. (En plotly puedes agregar la media con
boxmean=Trueenupdate_traces.) notched=Truedibuja una "muesca" alrededor de la mediana: si las muescas de dos cajas no se solapan, es un indicio de que sus medianas son realmente distintas.points='all'muestra cada dato; con miles de puntos puede tardar.
¿Por qué la mediana? Porque es robusta: Minecraft (70 millones) sube mucho el promedio de ingresos, pero casi no mueve la mediana. En datos con valores extremos (ingresos, precios de casas, seguidores), la mediana describe mejor "la app típica".
La escala logarítmica
En una escala logarítmica, cada marca del eje multiplica por 10: 1, 10, 100, 1 000, 10 000… Así caben en el mismo gráfico apps con 1 instalación y con mil millones.
| Escala | La misma distancia significa… | Úsala cuando… |
|---|---|---|
| Lineal | Sumar lo mismo (+100) | Los valores tienen magnitudes parecidas |
| Logarítmica | Multiplicar por lo mismo (×10) | Los valores abarcan varios órdenes de magnitud |
⚠️ Cuidado: el logaritmo de 0 no existe. Las apps gratuitas tienen
Revenue_Estimate = 0y las apps con 0 instalaciones desaparecen de un eje logarítmico sin aviso. Por eso el gráfico de ingresos usa solo apps pagas (verificado: ninguna app paga tiene 0 instalaciones tras la limpieza). Y avisa siempre a tu público cuando un eje es logarítmico: una diferencia que "se ve pequeña" puede ser de 100 veces.
9. 🚀 Proyecto del día: informe para una desarrolladora de apps
(Integra las lecciones 560 a 567.)
Problema
Una egresada del colegio quiere lanzar su primera app y pregunta: ¿en qué categoría?, ¿gratis o paga?, ¿a qué precio?
Contexto
Las decisiones de emprendimiento se toman mejor con datos. Pero los datos son de 2018, son una muestra y las instalaciones son aproximadas: el informe debe ser útil y honesto.
Objetivo
Un cuaderno con el análisis completo del curso y un informe de una página con una recomendación argumentada.
Requisitos funcionales
- Limpieza documentada: columnas eliminadas, faltantes, duplicados (con el
subsetjustificado), conversión deInstallsyPrice, datos basura. - Columna
Revenue_Estimate. - Mínimo 6 gráficos de plotly: dona, barras verticales, barras horizontales, dispersión, barras agrupadas y cajas.
- Géneros separados con
split+stack(oexplode). - Una recomendación final: categoría, modelo (gratis/paga) y rango de precio, cada una apoyada en un gráfico.
Restricciones técnicas
- Todos los gráficos con título y ejes en español.
- Cada escala logarítmica indicada en el título del eje.
- Guardar los gráficos con
fig.write_html().
Planificación
- Comprender la pregunta. 2. Entradas:
apps.csv. 3. Procesos: limpiar → convertir → filtrar → agregar → graficar. 4. Salidas: gráficos e informe. 5. Dividir en secciones por lección. 6. Bocetar a mano los 6 gráficos. 7. Implementar. 8. Probar con la tabla. 9. Corregir. 10. Redactar la recomendación.
Construcción paso a paso
- Versión 1: exploración, eliminación de columnas,
dropna. - Versión 2: duplicados con
subset; comprobación con Instagram. - Versión 3: conversiones numéricas, filtro de basura,
Revenue_Estimate. - Versión 4: gráficos de categorías (barras y dispersión) y géneros.
- Versión final: gratis contra pagas (barras agrupadas y cajas), recomendación.
Pruebas
| Comprobación | Valor esperado (verificado) |
|---|---|
Filas tras dropna |
9 367 |
| Filas tras quitar duplicados | 8 199 |
| Filas tras quitar apps de 250 dólares o más | 8 184 |
| Categorías / géneros | 33 / 53 |
| Mediana de instalaciones (gratis / pagas) | 500 000 / 5 000 |
| Precio mediano de las pagas | 2,99 |
Puedes automatizarlas con assert, por ejemplo: assert df_apps_clean.shape[0] == 8184.
🧪 Prueba y depuración
df_apps.drop(['Last_Updated', 'Android_Version'])
df_apps_clean = df_apps.dropna()
df_apps_clean = df_apps_clean.drop_duplicates()
df_apps_clean.Installs = pd.to_numeric(df_apps_clean.Installs)
df_apps_clean = df_apps_clean[df_apps_clean['Price'] < 250]
box = px.box(df_apps_clean, y='Revenue_Estimate', x='Category')
box.update_layout(yaxis=dict(type='log'))Encuentra seis problemas: drop sin axis=1 y con el nombre Android_Version (no existe, y además no se asigna el resultado); drop_duplicates() sin subset deja copias de la misma app; to_numeric sin quitar las comas lanza ValueError; se compara Price (todavía texto con $) con un número (TypeError); Revenue_Estimate no se ha creado; y el gráfico mezcla apps gratuitas (ingreso 0) con escala logarítmica, así que desaparecen sin aviso.
⭐ Extensión del proyecto
- Reto 1: agrega un filtro interactivo: guarda un gráfico por categoría en HTML y crea un índice.
- Reto 2: compara la calificación de las apps pagas y gratuitas de la misma categoría con cajas.
- Reto 3: investiga cómo ganan dinero hoy las apps gratuitas (publicidad, suscripciones, compras dentro de la app) y explica qué no pueden decir estos datos al respecto.
10. 🌟 Proyecto extra: las apps de nuestro curso
(Proyecto creado para esta adaptación; no existe en el curso original.)
Situación
El consejo estudiantil quiere saber qué apps usan los estudiantes para estudiar y para entretenerse, y si pagarían por alguna, para proponer talleres de uso responsable.
Requisitos
- Diseña con tu docente una encuesta anónima (Google Forms o papel) con: apps más usadas (hasta 3, separadas por
;), usos (estudio, redes, juegos, música… con casillas múltiples), horas por día y si has pagado por una app (sí/no). - Exporta las respuestas a CSV. Limpia: duplicados (¿alguien respondió dos veces?), nombres escritos de formas distintas ("whatsapp", "WhatsApp", "Whats app") y horas escritas como texto ("2 horas").
- Separa las columnas anidadas (apps y usos) con
split+stackoexplode. - Crea con plotly: una dona (¿has pagado por una app?), barras de las 10 apps más mencionadas, barras agrupadas de usos por grado y cajas de horas de uso.
- Escribe 3 conclusiones y 1 recomendación para el consejo estudiantil.
Decisiones que debes tomar (y justificar)
- ¿Cómo unificas los nombres de las apps? ¿Una función, un diccionario de equivalencias?
- ¿Qué haces con respuestas absurdas (30 horas por día)?
- ¿Tu encuesta representa a todo el colegio o solo a tu curso?
Casos de prueba
- Ninguna app aparece dos veces con nombres distintos en el gráfico final.
- La columna de horas es numérica y está entre 0 y 24.
- Ningún dato permite identificar a una persona.
11. 🏗️ Laboratorio de Datos Abiertos — Etapa 1
(Proyecto integrador creado para esta adaptación. Se construye por etapas durante los Días 76 a 81 y se entrega completo en el Día 81. Continúa el trabajo del Observatorio de Datos del Colegio, Días 72 a 75.)
La idea: elegir un conjunto de datos abiertos colombianos (datos.gov.co, DANE, ICFES, IDEAM…) y llevarlo, paso a paso, desde la pregunta hasta un modelo que haga estimaciones, con gráficos interactivos y una prueba estadística.
Etapa 1 (hoy):
- Elige un tema y un conjunto de datos con al menos 500 filas, una variable numérica que quieras explicar (por ejemplo, un puntaje, un precio, una cantidad) y varias variables que podrían influir en ella. Ejemplos: resultados Saber 11 por municipio, calidad del aire, precios de productos agrícolas.
- Escribe tres preguntas que quieras responder.
- Limpia: duplicados (con
subsetjustificado), columnas numéricas guardadas como texto, datos basura. - Crea tres gráficos de plotly (uno de partes de un todo, uno de barras y uno de cajas) y guárdalos en HTML.
- Registra en
FUENTES.mdla URL, la fecha de descarga y la licencia.
Producto de la etapa: laboratorio.ipynb (secciones 1 y 2), data/, FUENTES.md.
12. 🧩 Comprueba lo aprendido
Conceptuales
- ¿Qué diferencia hay entre un gráfico de Matplotlib y uno de plotly? ¿Cuándo prefieres cada uno?
- ¿Por qué
drop_duplicates()sinsubsetno eliminó todas las copias de Instagram? - ¿Qué muestra la línea del medio de un diagrama de caja? ¿Por qué no es el promedio?
- ¿Por qué
Genres.nunique()da 114 si solo hay 53 géneros? - ¿Qué supuestos hay detrás de
Revenue_Estimate?
Análisis de código
df_free_vs_paid = df_apps_clean.groupby(["Category", "Type"], as_index=False).agg({'App': 'count'})¿Cuántas columnas tiene el resultado? ¿Qué cambia si quitas as_index=False?
Predicción
Si s = pd.Series(['A;B', 'B', 'A;C;B']), ¿qué devuelve s.str.split(';', expand=True).stack().value_counts()?
Depuración
"Mi gráfico de barras horizontales muestra la categoría más popular abajo". ¿Por qué y cómo lo arreglas?
Modificación
Cambia el diagrama de caja de precios para mostrar también la media de cada categoría.
Creación
Escribe una función limpiar_numero(serie, simbolos) que quite todos los símbolos de la lista y convierta la serie a número. Pruébala con Installs y con Price.
13. 🤖 Programar con apoyo de IA
Ciclo de trabajo: formula el problema → inténtalo tú → consulta a la IA → lee y entiende → prueba → compara con la documentación oficial → modifica → explica con tus palabras.
Actividad 1 — ¿Qué es un duplicado? Pide a una IA el código para eliminar duplicados de df_apps_clean. Si responde solo drop_duplicates(), comprueba cuántas filas de Instagram quedan y explícale el problema. Compara con la documentación de DataFrame.drop_duplicates (parámetros subset y keep).
Actividad 2 — Verificar un gráfico. Pide a una IA "un gráfico de caja de ingresos por categoría en plotly con escala logarítmica" sobre todas las apps. ¿Advierte que las apps gratuitas (ingreso 0) desaparecen? Comprueba contando cuántas filas tienen ingreso 0.
Actividad 3 — Datos que cambian. Pregunta a una IA cuál es el tamaño máximo de una app en Google Play. Compara su respuesta con los datos de 2018 (100 MB) y con la documentación oficial vigente de Play Console. Anota qué fuente es más confiable para cada pregunta.
14. 🛠️ Correcciones técnicas al material original
| Archivo | Afirmación o comportamiento del curso | Observación | Tratamiento |
|---|---|---|---|
560 |
Eliminar la columna Android_Version |
Se llama Android_Ver; con el otro nombre, KeyError (verificado) |
Nota técnica de la sección 1 |
560 |
drop_duplicates con columnas (solo en imagen) |
Reconstruido y verificado: subset=['App', 'Type', 'Price'] da 8 199 |
Sección 1 |
560 |
Los NaN de calificación van con apps sin reseñas | Cierto para la mayoría (596 con 0 reseñas; 85 % con 10 o menos), no para todas (máximo 3 248) | Nota técnica de la sección 1 |
563 |
La columna es de tipo object |
En pandas 3 es str (verificado) |
Nota técnica de la sección 4 |
563 |
4 de las 5 apps más caras son médicas | 3 de 5; el quinto puesto es un empate a 29,99 (verificado) | Nota técnica de la sección 4 |
563 |
El filtro < 250 elimina las apps basura |
Quedan "I am Rich Person" (37,99) e "I am Rich Premium Plus" (18,99) (verificado) | Sección 4 y reto 2 |
566 |
El diagrama de caja muestra "la mediana, el máximo, el mínimo y los cuartiles" | Correcto; los bigotes no llegan al máximo cuando hay atípicos | Sección 8 |
566 |
30 000 dólares como costo de desarrollo | Referencia externa del curso; no se verificó el enlace | Sección 7 |
559 |
Límite de 100 MB | Válido para los datos de 2018; las reglas actuales de la tienda son otras | Sección 2 |
15. 📋 Matriz de trazabilidad
| Contenido original | Archivo de origen | Concepto desarrollado | Ejercicio original | Reto adicional | Proyecto relacionado |
|---|---|---|---|---|---|
| Analítica de tiendas de apps, preguntas del día, qué se aprende, cuaderno y CSV | 559 |
Sección 0 | Preparar el cuaderno | — | Sección 9 |
shape, sample, eliminar columnas, isna, dropna, duplicated, Instagram, drop_duplicates con columnas, supuestos de los datos |
560 |
Sección 1 | Retos de exploración, NaN y duplicados | Retos 1–3 de la sección 1 | Secciones 9, 10 y 11 |
| Mejor calificación, mayor tamaño, más reseñas, límite de 100 MB, ninguna app paga en el top 50 | 561 |
Sección 2 | Tres retos de exploración | Puntaje de confianza | Sección 9 |
value_counts, px.pie, update_traces, dona con hole |
562 |
Sección 3 | Gráfico de clasificación de contenido | Retos 1–3 de la sección 3 | Secciones 9 y 10 |
Conversión de Installs y Price, groupby por instalaciones, apps "I am Rich", filtro < 250, Revenue_Estimate, Minecraft |
563 |
Sección 4 | Dos retos de conversión | Retos 1–4 de la sección 4 | Sección 9 |
nunique, barras verticales y horizontales, update_layout, merge, dispersión con size, color, hover_name y escala logarítmica |
564 |
Secciones 5 y 8 | Reto del gráfico de dispersión | Concentración por app | Sección 9 |
Géneros anidados, split(expand=True), stack, escalas de color, coloraxis_showscale |
565 |
Sección 6 | Dos retos de géneros | explode; retos 1–3 |
Secciones 9 y 10 |
Gratis contra pagas, as_index=False, barras agrupadas, categoryorder, cajas de instalaciones, ingresos y precios |
566 |
Secciones 7 y 8 | Cuatro retos | Media en la caja | Secciones 9, 10 y 11 |
| Resumen del día, cuaderno completo | 567 |
Sección 7 | — | — | Sección 9 |
| (No existe en el curso original) | — | Sección 8 | — | Diagrama de caja y escala logarítmica | Secciones 9 y 11 |
| (No existe en el curso original) | — | — | — | Las apps de nuestro curso | Sección 10 |
| (No existe en el curso original) | — | — | — | Laboratorio de Datos Abiertos, etapa 1 | Sección 11 |
16. 📦 Evidencias de aprendizaje del Día 76
- El cuaderno del análisis de Google Play con los 6 gráficos de plotly guardados en HTML.
- El informe de una página con la recomendación para la desarrolladora.
- El análisis de la encuesta de apps del curso (proyecto extra).
- La etapa 1 del Laboratorio de Datos Abiertos: tema, preguntas, datos limpios, 3 gráficos y
FUENTES.md. - Cuaderno de errores:
KeyErrorpor nombre de columna,dropsinaxis=1,drop_duplicatessinsubset,to_numericcon comas, comparar texto con números, barras horizontales invertidas, ceros en escala logarítmica, confundir mediana y media.