Saltar al contenido
Volver al inicio 100 Días de Código en Python — Día 76: Plotly y la tienda de Google Play — gráficos interactivos para tomar decisiones

100 Días de Código en Python — Día 76: Plotly y la tienda de Google Play — gráficos interactivos para tomar decisiones

Nivel: Grado décimo Área: Tecnología e Informática Modalidad: Tutorial práctico Fuente: Curso "100 Days of Code: The Complete Python Pro Bootcamp" (Angela Yu, App Brewery). Contenido del Día 76 (nivel avanzado).

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.csv está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 eliminar Android_Version, pero la columna se llama Android_Ver (KeyError); en pandas 3 las columnas de texto ya no son de tipo object sino str; 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

  1. Antes de empezar: ¿vale la pena crear una app?
  2. Explorar y limpiar: faltantes y duplicados
  3. Primeras preguntas: calificación, tamaño y reseñas
  4. Plotly: gráficos circulares y de dona
  5. De texto a número: instalaciones y precios
  6. Barras y dispersión: categorías competidas y populares
  7. Datos anidados: split y stack
  8. Barras agrupadas y diagramas de caja
  9. 📖 Leer un diagrama de caja y la escala logarítmica
  10. 🚀 Proyecto del día: informe para una desarrolladora de apps
  11. 🌟 Proyecto extra: las apps de nuestro curso
  12. 🏗️ Laboratorio de Datos Abiertos — Etapa 1
  13. 🧩 Comprueba lo aprendido
  14. 🤖 Programar con apoyo de IA
  15. 🛠️ Correcciones técnicas al material original
  16. 📋 Matriz de trazabilidad
  17. 📦 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, agg y merge (Día 74). Nuevo hoy: la biblioteca plotly. 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.

Python
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().

Python
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:

Python
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 llama Android_Ver; con el nombre del enunciado aparece KeyError: "['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 de Rating, porque la única fila sin Type tampoco 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 Reviews antes de drop_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?

Python
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.

Python
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:

Python
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:

  1. Parámetros de la función (title, names, hole, color…).
  2. fig.update_traces(...): cambia las marcas (texto, tamaño, bordes).
  3. 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

  • hole fuera de rango: debe estar entre 0 y 1; hole=1.2 produce ValueError.
  • Olvidar fig.show() fuera de un cuaderno: el gráfico se crea pero no se ve.
  • Pasar values sin names: 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.

Python
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?

Python
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:

Python
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:

Texto
"1,000,000"  --(.str.replace(',', ''))-->  "1000000"  --(pd.to_numeric)-->  1000000
"$4.99"      --(.str.replace('$', ''))-->  "4.99"     --(pd.to_numeric)-->  4.99

Si 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 (antes object). El diagnóstico es el mismo: "no es numérica". Y .str.replace('$', '') trata el $ literalmente porque desde pandas 2.0 regex=False es 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_values deje 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:

Python
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:

Python
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')).

Python
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.sum o 'sum': agg({'Installs': pd.Series.sum}) funciona, pero la forma habitual y más corta es agg({'Installs': 'sum'}) o groupby('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.
  • merge sin columna común: aquí Category es el índice de ambos resultados; on='Category' funciona porque pandas acepta nombres de índice. Si fallara, usa left_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.

Python
# 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)}')    # 53

Reto: el gráfico de los 15 géneros principales con escalas de color (color_continuous_scale) y sin eje de color (coloraxis_showscale):

Python
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)

Texto
Genres                     split(expand=True)          stack()
"Puzzle;Brain Games"  -->  | Puzzle | Brain Games |  -->  Puzzle
"Tools"               -->  | Tools  | None        |  -->  Brain Games
                                                        Tools
  1. .str.split(';', expand=True) crea una columna por género (las celdas sobrantes quedan vacías).
  2. .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:

Python
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 con assert que da el mismo resultado que stack().

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:

Python
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':

Python
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.

Python
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?

Python
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': group pone las barras lado a lado; stack, una encima de otra (por defecto en px.bar con color).
  • categoryorder mal escrito: un valor inválido lanza ValueError con 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

Texto
      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=True en update_traces.)
  • notched=True dibuja 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 = 0 y 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

  1. Limpieza documentada: columnas eliminadas, faltantes, duplicados (con el subset justificado), conversión de Installs y Price, datos basura.
  2. Columna Revenue_Estimate.
  3. Mínimo 6 gráficos de plotly: dona, barras verticales, barras horizontales, dispersión, barras agrupadas y cajas.
  4. Géneros separados con split + stack (o explode).
  5. 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

  1. 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

Python
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

  1. 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).
  2. 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").
  3. Separa las columnas anidadas (apps y usos) con split + stack o explode.
  4. 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.
  5. 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):

  1. 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.
  2. Escribe tres preguntas que quieras responder.
  3. Limpia: duplicados (con subset justificado), columnas numéricas guardadas como texto, datos basura.
  4. Crea tres gráficos de plotly (uno de partes de un todo, uno de barras y uno de cajas) y guárdalos en HTML.
  5. Registra en FUENTES.md la 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

  1. ¿Qué diferencia hay entre un gráfico de Matplotlib y uno de plotly? ¿Cuándo prefieres cada uno?
  2. ¿Por qué drop_duplicates() sin subset no eliminó todas las copias de Instagram?
  3. ¿Qué muestra la línea del medio de un diagrama de caja? ¿Por qué no es el promedio?
  4. ¿Por qué Genres.nunique() da 114 si solo hay 53 géneros?
  5. ¿Qué supuestos hay detrás de Revenue_Estimate?

Análisis de código

Python
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: KeyError por nombre de columna, drop sin axis=1, drop_duplicates sin subset, to_numeric con comas, comparar texto con números, barras horizontales invertidas, ceros en escala logarítmica, confundir mediana y media.