Sprint 3: Semana 2 - Joins y métricas financieras en SQL#
Propósito: usar SQL para calcular métricas financieras (ingresos, costos, margen, ROI), estructurar y comunicar resultados, y analizar tendencias.
Agenda#
Review de conceptos clave de SQL (Joins, Agregaciones)
Calculating Key Financial Metrics
Ingresos, Costos, Margen Bruto
ROI (Return on Investment)
Análisis de tendencias y comparaciones año a año
Relaciones entre tablas JOIN#
Para refrescar los conceptos de joins, repasemos las relaciones entre tablas en nuestra base de datos Chinook:
Uno a Uno (1:1) 🔗 Una fila en una tabla se relaciona con una sola fila en otra tabla.
Uno a Muchos (1:N) 🔗 Una fila en una tabla se relaciona con muchas filas en otra tabla.
Muchos a Muchos (N:N) 🔗 Muchas filas en una tabla se relacionan con muchas filas en otra.
➡️ Con una tabla intermedia (tabla de asociación)
EN NUESTRA DATABASE CHINOOK,#
¿Puedes identificar los 3 tipos de relaciones?
¿Qué es un JOIN?#
Un JOIN permite combinar columnas de dos o más tablas relacionadas mediante una clave común (como CustomerId, ArtistId, etc.).
Tipo de JOIN |
Qué hace |
|---|---|
|
Devuelve solo las filas que coinciden en ambas tablas |
|
Devuelve todas las filas de la tabla izquierda, y las coincidentes (o NULL) |
|
Devuelve todas las filas de la derecha (solo en bases que lo soportan) |
|
Devuelve todas las filas, coincidan o no (SQLite no lo soporta directamente) |

INNER JOIN#
Muestra el nombre de las canciones y el nombre del género al que pertenecen.
import sqlite3
import pandas as pd
# Hacemos la conexión a la Database
conn = sqlite3.connect("chinook.db")
query='''
SELECT
t.Name AS cancion,
g.Name AS genero
FROM track t
INNER JOIN genre g ON t.GenreId = g.GenreId;
'''
pd.read_sql(query,con=conn)
| cancion | genero | |
|---|---|---|
| 0 | For Those About To Rock (We Salute You) | Rock |
| 1 | Balls to the Wall | Rock |
| 2 | Fast As a Shark | Rock |
| 3 | Restless and Wild | Rock |
| 4 | Princess of the Dawn | Rock |
| ... | ... | ... |
| 3498 | Pini Di Roma (Pinien Von Rom) \ I Pini Della V... | Classical |
| 3499 | String Quartet No. 12 in C Minor, D. 703 "Quar... | Classical |
| 3500 | L'orfeo, Act 3, Sinfonia (Orchestra) | Classical |
| 3501 | Quintet for Horn, Violin, 2 Violas, and Cello ... | Classical |
| 3502 | Koyaanisqatsi | Soundtrack |
3503 rows × 2 columns
LEFT JOIN#
Muestra todos los clientes y el total de sus compras (si las tienen).
query='''
SELECT
c.CustomerId AS id_cliente,
c.FirstName AS nombre,
c.LastName AS apellido,
i.Total AS total_compra
FROM customer c
LEFT JOIN invoice i ON c.CustomerId = i.CustomerId;
'''
pd.read_sql(query,con=conn)
| id_cliente | nombre | apellido | total_compra | |
|---|---|---|---|---|
| 0 | 1 | Luís | Gonçalves | 3.98 |
| 1 | 1 | Luís | Gonçalves | 3.96 |
| 2 | 1 | Luís | Gonçalves | 5.94 |
| 3 | 1 | Luís | Gonçalves | 0.99 |
| 4 | 1 | Luís | Gonçalves | 1.98 |
| ... | ... | ... | ... | ... |
| 407 | 59 | Puja | Srivastava | 5.94 |
| 408 | 59 | Puja | Srivastava | 1.99 |
| 409 | 59 | Puja | Srivastava | 1.98 |
| 410 | 59 | Puja | Srivastava | 13.86 |
| 411 | 59 | Puja | Srivastava | 8.91 |
412 rows × 4 columns
INNER JOIN: canciones con su álbum#
Muestra el nombre de cada canción junto al título del álbum al que pertenece.
query='''
SELECT
t.Name AS cancion,
al.Title AS album
FROM track t
INNER JOIN album al ON t.AlbumId = al.AlbumId;
'''
pd.read_sql(query,con=conn)
| cancion | album | |
|---|---|---|
| 0 | For Those About To Rock (We Salute You) | For Those About To Rock We Salute You |
| 1 | Balls to the Wall | Balls to the Wall |
| 2 | Fast As a Shark | Restless and Wild |
| 3 | Restless and Wild | Restless and Wild |
| 4 | Princess of the Dawn | Restless and Wild |
| ... | ... | ... |
| 3498 | Pini Di Roma (Pinien Von Rom) \ I Pini Della V... | Respighi:Pines of Rome |
| 3499 | String Quartet No. 12 in C Minor, D. 703 "Quar... | Schubert: The Late String Quartets & String Qu... |
| 3500 | L'orfeo, Act 3, Sinfonia (Orchestra) | Monteverdi: L'Orfeo |
| 3501 | Quintet for Horn, Violin, 2 Violas, and Cello ... | Mozart: Chamber Music |
| 3502 | Koyaanisqatsi | Koyaanisqatsi (Soundtrack from the Motion Pict... |
3503 rows × 2 columns
JOIN de tres tablas: factura + línea + canción#
Muestra el número de factura, el nombre de la canción comprada y el precio unitario.
query='''
SELECT
i.InvoiceId,
t.Name AS cancion,
ii.UnitPrice
FROM invoiceLine ii
JOIN invoice i ON ii.InvoiceId = i.InvoiceId
JOIN track t ON ii.TrackId = t.TrackId;
'''
pd.read_sql(query,con=conn)
| InvoiceId | cancion | UnitPrice | |
|---|---|---|---|
| 0 | 1 | Balls to the Wall | 0.99 |
| 1 | 1 | Restless and Wild | 0.99 |
| 2 | 2 | Put The Finger On You | 0.99 |
| 3 | 2 | Inject The Venom | 0.99 |
| 4 | 2 | Evil Walks | 0.99 |
| ... | ... | ... | ... |
| 2235 | 411 | Looking For Love | 0.99 |
| 2236 | 411 | Sweet Lady Luck | 0.99 |
| 2237 | 411 | Feirinha da Pavuna/Luz do Repente/Bagaço da La... | 0.99 |
| 2238 | 411 | Samba pras moças | 0.99 |
| 2239 | 412 | Hot Girl | 1.99 |
2240 rows × 3 columns
UNION VS UNIONALL#
Operador |
Qué hace |
|---|---|
|
Une los resultados y elimina duplicados |
|
Une los resultados y conserva todos los duplicados |
📌 Regla importante: Ambas consultas deben tener el mismo número de columnas y tipos compatibles.
Ejemplo Correos de clientes y empleados (UNION ALL)#
Obtén todos los correos, incluyendo los que puedan repetirse.
query='''
SELECT Email FROM customer
UNION
SELECT Email FROM employee;
'''
pd.read_sql(query,con=conn)
| 0 | aaronmitchell@yahoo.ca |
|---|---|
| 1 | alero@uol.com.br |
| 2 | andrew@chinookcorp.com |
| 3 | astrid.gruber@apple.at |
| 4 | bjorn.hansen@yahoo.no |
| ... | ... |
| 62 | steve@chinookcorp.com |
| 63 | terhi.hamalainen@apple.fi |
| 64 | tgoyer@apple.com |
| 65 | vstevens@yahoo.com |
| 66 | wyatt.girard@yahoo.fr |
67 rows × 1 columns
Ejemplo correos de clientes y empleados (UNION ALL)#
Obtén todos los correos, incluyendo los que puedan repetirse.
query='''
SELECT Email FROM customer
UNION ALL
SELECT Email FROM employee;
'''
pd.read_sql(query,con=conn)
| 0 | luisg@embraer.com.br |
|---|---|
| 1 | leonekohler@surfeu.de |
| 2 | ftremblay@gmail.com |
| 3 | bjorn.hansen@yahoo.no |
| 4 | frantisekw@jetbrains.com |
| ... | ... |
| 62 | margaret@chinookcorp.com |
| 63 | steve@chinookcorp.com |
| 64 | michael@chinookcorp.com |
| 65 | robert@chinookcorp.com |
| 66 | laura@chinookcorp.com |
67 rows × 1 columns
Ejercicios en clase:#
#Empleados que atienden más clientes
query='''
SELECT
e.EmployeeId,
e.FirstName,
e.LastName,
COUNT(c.CustomerId) AS num_customers
FROM Employee AS e
JOIN Customer AS c
ON c.SupportRepId = e.EmployeeId
GROUP BY
e.EmployeeId,
e.FirstName,
e.LastName
ORDER BY num_customers DESC;
'''
pd.read_sql(query,con=conn)
| EmployeeId | FirstName | LastName | num_customers | |
|---|---|---|---|---|
| 0 | 3 | Jane | Peacock | 21 |
| 1 | 4 | Margaret | Park | 20 |
| 2 | 5 | Steve | Johnson | 18 |
# 3. Artistas con más álbumes
query = '''
SELECT
ar.ArtistId,
ar.Name AS ArtistName,
COUNT(al.AlbumId) AS num_albums
FROM Artist AS ar
JOIN Album AS al
ON al.ArtistId = ar.ArtistId
GROUP BY
ar.ArtistId,
ar.Name
ORDER BY num_albums DESC
LIMIT 10;
'''
pd.read_sql(query, con=conn)
| ArtistId | ArtistName | num_albums | |
|---|---|---|---|
| 0 | 90 | Iron Maiden | 21 |
| 1 | 22 | Led Zeppelin | 14 |
| 2 | 58 | Deep Purple | 11 |
| 3 | 50 | Metallica | 10 |
| 4 | 150 | U2 | 10 |
| 5 | 114 | Ozzy Osbourne | 6 |
| 6 | 118 | Pearl Jam | 5 |
| 7 | 21 | Various Artists | 4 |
| 8 | 82 | Faith No More | 4 |
| 9 | 84 | Foo Fighters | 4 |
# 4. Géneros que generan más ventas
query = '''
SELECT
g.GenreId,
g.Name AS GenreName,
SUM(il.UnitPrice * il.Quantity) AS revenue
FROM Genre AS g
JOIN Track AS t
ON t.GenreId = g.GenreId
JOIN InvoiceLine AS il
ON il.TrackId = t.TrackId
GROUP BY
g.GenreId,
g.Name
ORDER BY revenue DESC;
'''
pd.read_sql(query, con=conn)
| GenreId | GenreName | revenue | |
|---|---|---|---|
| 0 | 1 | Rock | 826.65 |
| 1 | 7 | Latin | 382.14 |
| 2 | 3 | Metal | 261.36 |
| 3 | 4 | Alternative & Punk | 241.56 |
| 4 | 19 | TV Shows | 93.53 |
| 5 | 2 | Jazz | 79.20 |
| 6 | 6 | Blues | 60.39 |
| 7 | 21 | Drama | 57.71 |
| 8 | 14 | R&B/Soul | 40.59 |
| 9 | 24 | Classical | 40.59 |
| 10 | 20 | Sci Fi & Fantasy | 39.80 |
| 11 | 8 | Reggae | 29.70 |
| 12 | 9 | Pop | 27.72 |
| 13 | 10 | Soundtrack | 19.80 |
| 14 | 22 | Comedy | 17.91 |
| 15 | 17 | Hip Hop/Rap | 16.83 |
| 16 | 11 | Bossa Nova | 14.85 |
| 17 | 23 | Alternative | 13.86 |
| 18 | 16 | World | 12.87 |
| 19 | 18 | Science Fiction | 11.94 |
| 20 | 13 | Heavy Metal | 11.88 |
| 21 | 15 | Electronica/Dance | 11.88 |
| 22 | 12 | Easy Listening | 9.90 |
| 23 | 5 | Rock And Roll | 5.94 |
# 5. Clientes que más gastan
query = '''
SELECT
c.CustomerId,
c.FirstName,
c.LastName,
c.Country,
SUM(i.Total) AS total_spent
FROM Customer AS c
JOIN Invoice AS i
ON i.CustomerId = c.CustomerId
GROUP BY
c.CustomerId,
c.FirstName,
c.LastName,
c.Country
ORDER BY total_spent DESC;
'''
pd.read_sql(query, con=conn)
| CustomerId | FirstName | LastName | Country | total_spent | |
|---|---|---|---|---|---|
| 0 | 6 | Helena | Holý | Czech Republic | 49.62 |
| 1 | 26 | Richard | Cunningham | USA | 47.62 |
| 2 | 57 | Luis | Rojas | Chile | 46.62 |
| 3 | 45 | Ladislav | Kovács | Hungary | 45.62 |
| 4 | 46 | Hugh | O'Reilly | Ireland | 45.62 |
| 5 | 24 | Frank | Ralston | USA | 43.62 |
| 6 | 28 | Julia | Barnett | USA | 43.62 |
| 7 | 37 | Fynn | Zimmermann | Germany | 43.62 |
| 8 | 7 | Astrid | Gruber | Austria | 42.62 |
| 9 | 25 | Victor | Stevens | USA | 42.62 |
| 10 | 44 | Terhi | Hämäläinen | Finland | 41.62 |
| 11 | 5 | František | Wichterlová | Czech Republic | 40.62 |
| 12 | 43 | Isabelle | Mercier | France | 40.62 |
| 13 | 48 | Johannes | Van der Berg | Netherlands | 40.62 |
| 14 | 1 | Luís | Gonçalves | Brazil | 39.62 |
| 15 | 3 | François | Tremblay | Canada | 39.62 |
| 16 | 4 | Bjørn | Hansen | Norway | 39.62 |
| 17 | 17 | Jack | Smith | USA | 39.62 |
| 18 | 20 | Dan | Miller | USA | 39.62 |
| 19 | 22 | Heather | Leacock | USA | 39.62 |
| 20 | 34 | João | Fernandes | Portugal | 39.62 |
| 21 | 42 | Wyatt | Girard | France | 39.62 |
| 22 | 15 | Jennifer | Peterson | Canada | 38.62 |
| 23 | 19 | Tim | Goyer | USA | 38.62 |
| 24 | 39 | Camille | Bernard | France | 38.62 |
| 25 | 40 | Dominique | Lefebvre | France | 38.62 |
| 26 | 51 | Joakim | Johansson | Sweden | 38.62 |
| 27 | 58 | Manoj | Pareek | India | 38.62 |
| 28 | 2 | Leonie | Köhler | Germany | 37.62 |
| 29 | 8 | Daan | Peeters | Belgium | 37.62 |
| 30 | 9 | Kara | Nielsen | Denmark | 37.62 |
| 31 | 10 | Eduardo | Martins | Brazil | 37.62 |
| 32 | 11 | Alexandre | Rocha | Brazil | 37.62 |
| 33 | 12 | Roberto | Almeida | Brazil | 37.62 |
| 34 | 13 | Fernanda | Ramos | Brazil | 37.62 |
| 35 | 14 | Mark | Philips | Canada | 37.62 |
| 36 | 16 | Frank | Harris | USA | 37.62 |
| 37 | 18 | Michelle | Brooks | USA | 37.62 |
| 38 | 21 | Kathy | Chase | USA | 37.62 |
| 39 | 23 | John | Gordon | USA | 37.62 |
| 40 | 27 | Patrick | Gray | USA | 37.62 |
| 41 | 29 | Robert | Brown | Canada | 37.62 |
| 42 | 30 | Edward | Francis | Canada | 37.62 |
| 43 | 31 | Martha | Silk | Canada | 37.62 |
| 44 | 32 | Aaron | Mitchell | Canada | 37.62 |
| 45 | 33 | Ellie | Sullivan | Canada | 37.62 |
| 46 | 35 | Madalena | Sampaio | Portugal | 37.62 |
| 47 | 36 | Hannah | Schneider | Germany | 37.62 |
| 48 | 38 | Niklas | Schröder | Germany | 37.62 |
| 49 | 41 | Marc | Dubois | France | 37.62 |
| 50 | 47 | Lucas | Mancini | Italy | 37.62 |
| 51 | 49 | Stanisław | Wójcik | Poland | 37.62 |
| 52 | 50 | Enrique | Muñoz | Spain | 37.62 |
| 53 | 52 | Emma | Jones | United Kingdom | 37.62 |
| 54 | 53 | Phil | Hughes | United Kingdom | 37.62 |
| 55 | 54 | Steve | Murray | United Kingdom | 37.62 |
| 56 | 55 | Mark | Taylor | Australia | 37.62 |
| 57 | 56 | Diego | Gutiérrez | Argentina | 37.62 |
| 58 | 59 | Puja | Srivastava | India | 36.64 |
# 6. Empleados que han vendido más (facturas)
query = '''
SELECT
e.EmployeeId,
e.FirstName,
e.LastName,
SUM(i.Total) AS total_sales
FROM Employee AS e
JOIN Customer AS c
ON c.SupportRepId = e.EmployeeId
JOIN Invoice AS i
ON i.CustomerId = c.CustomerId
GROUP BY
e.EmployeeId,
e.FirstName,
e.LastName
ORDER BY total_sales DESC;
'''
pd.read_sql(query, con=conn)
| EmployeeId | FirstName | LastName | total_sales | |
|---|---|---|---|---|
| 0 | 3 | Jane | Peacock | 833.04 |
| 1 | 4 | Margaret | Park | 775.40 |
| 2 | 5 | Steve | Johnson | 720.16 |
# 7. Álbumes más vendidos
query = '''
SELECT
al.AlbumId,
al.Title,
COUNT(il.InvoiceLineId) AS num_items,
SUM(il.Quantity) AS total_tracks_sold
FROM Album AS al
JOIN Track AS t
ON t.AlbumId = al.AlbumId
JOIN InvoiceLine AS il
ON il.TrackId = t.TrackId
GROUP BY
al.AlbumId,
al.Title
ORDER BY total_tracks_sold DESC;
'''
pd.read_sql(query, con=conn)
| AlbumId | Title | num_items | total_tracks_sold | |
|---|---|---|---|---|
| 0 | 23 | Minha Historia | 27 | 27 |
| 1 | 141 | Greatest Hits | 26 | 26 |
| 2 | 73 | Unplugged | 25 | 25 |
| 3 | 224 | Acústico | 22 | 22 |
| 4 | 37 | Greatest Kiss | 20 | 20 |
| ... | ... | ... | ... | ... |
| 299 | 335 | J.S. Bach: Chaconne, Suite in E Minor, Partita... | 1 | 1 |
| 300 | 337 | Szymanowski: Piano Works, Vol. 1 | 1 | 1 |
| 301 | 338 | Nielsen: The Six Symphonies | 1 | 1 |
| 302 | 340 | Liszt - 12 Études D'Execution Transcendante | 1 | 1 |
| 303 | 343 | Respighi:Pines of Rome | 1 | 1 |
304 rows × 4 columns
# 8a. Número de canciones por playlist
query = '''
SELECT
pl.PlaylistId,
pl.Name AS PlaylistName,
COUNT(pt.TrackId) AS num_tracks
FROM Playlist AS pl
JOIN PlaylistTrack AS pt
ON pt.PlaylistId = pl.PlaylistId
GROUP BY
pl.PlaylistId,
pl.Name
ORDER BY num_tracks DESC;
'''
pd.read_sql(query, con=conn)
| PlaylistId | PlaylistName | num_tracks | |
|---|---|---|---|
| 0 | 1 | Music | 3290 |
| 1 | 8 | Music | 3290 |
| 2 | 5 | 90’s Music | 1477 |
| 3 | 3 | TV Shows | 213 |
| 4 | 10 | TV Shows | 213 |
| 5 | 12 | Classical | 75 |
| 6 | 11 | Brazilian Music | 39 |
| 7 | 17 | Heavy Metal Classic | 26 |
| 8 | 13 | Classical 101 - Deep Cuts | 25 |
| 9 | 14 | Classical 101 - Next Steps | 25 |
| 10 | 15 | Classical 101 - The Basics | 25 |
| 11 | 16 | Grunge | 15 |
| 12 | 9 | Music Videos | 1 |
| 13 | 18 | On-The-Go 1 | 1 |
# 8b. Géneros por playlist
query = '''
SELECT
pl.PlaylistId,
pl.Name AS PlaylistName,
g.Name AS GenreName,
COUNT(t.TrackId) AS num_tracks
FROM Playlist AS pl
JOIN PlaylistTrack AS pt
ON pt.PlaylistId = pl.PlaylistId
JOIN Track AS t
ON t.TrackId = pt.TrackId
JOIN Genre AS g
ON g.GenreId = t.GenreId
GROUP BY
pl.PlaylistId,
pl.Name,
g.Name
ORDER BY
pl.Name,
num_tracks DESC;
'''
pd.read_sql(query, con=conn)
| PlaylistId | PlaylistName | GenreName | num_tracks | |
|---|---|---|---|---|
| 0 | 5 | 90’s Music | Rock | 621 |
| 1 | 5 | 90’s Music | Latin | 257 |
| 2 | 5 | 90’s Music | Alternative & Punk | 193 |
| 3 | 5 | 90’s Music | Metal | 164 |
| 4 | 5 | 90’s Music | Classical | 40 |
| ... | ... | ... | ... | ... |
| 77 | 10 | TV Shows | Sci Fi & Fantasy | 26 |
| 78 | 3 | TV Shows | Comedy | 17 |
| 79 | 10 | TV Shows | Comedy | 17 |
| 80 | 3 | TV Shows | Science Fiction | 13 |
| 81 | 10 | TV Shows | Science Fiction | 13 |
82 rows × 4 columns
# 9. Clientes que compraron canciones de un artista específico
query = '''
SELECT DISTINCT
c.CustomerId,
c.FirstName,
c.LastName,
c.Email,
c.Country
FROM Customer AS c
JOIN Invoice AS i
ON i.CustomerId = c.CustomerId
JOIN InvoiceLine AS il
ON il.InvoiceId = i.InvoiceId
JOIN Track AS t
ON t.TrackId = il.TrackId
JOIN Album AS al
ON al.AlbumId = t.AlbumId
JOIN Artist AS ar
ON ar.ArtistId = al.ArtistId
WHERE ar.Name = 'AC/DC'
ORDER BY c.Country, c.LastName;
'''
pd.read_sql(query, con=conn)
| CustomerId | FirstName | LastName | Country | ||
|---|---|---|---|---|---|
| 0 | 8 | Daan | Peeters | daan_peeters@apple.be | Belgium |
| 1 | 13 | Fernanda | Ramos | fernadaramos4@uol.com.br | Brazil |
| 2 | 33 | Ellie | Sullivan | ellie.sullivan@shaw.ca | Canada |
| 3 | 47 | Lucas | Mancini | lucas.mancini@yahoo.it | Italy |
| 4 | 4 | Bjørn | Hansen | bjorn.hansen@yahoo.no | Norway |
| 5 | 53 | Phil | Hughes | phil.hughes@gmail.com | United Kingdom |
KPIs de negocio#
KPIs de Negocio: Ingresos, Costos, Margen Bruto, ROI y Análisis de Tendencias
1. Ingresos, Costos y Margen Bruto
1.1 Ingresos (Revenue)
Los ingresos son el valor total de las ventas en un periodo. Es el dinero que entra antes de restar ningún tipo de costo.
Revenue = Precio de venta × Cantidad vendida
Ejemplo:
- Precio por unidad: 50 USD
- Cantidad vendida en el mes: 500 unidades
Cálculo:
Revenue(Ingresos) = 50 × 500 = 25 000 USD
1.2 Costos
Los costos son todos los gastos necesarios para producir u ofrecer un producto o servicio.
No cambian según cuántas unidades se produzcan o vendan.
(Ej.: alquiler, salarios administrativos, seguros, servicios generales).
Cambian según el volumen producido o vendido.
(Ej.: materia prima, empaques, comisiones por venta).
Costo Total = Costos Fijos + Costos Variables
Esta fórmula describe los costos operativos generales de la empresa.
Útil para analizar eficiencia, punto de equilibrio y rentabilidad global.
El cálculo de Margen Bruto no utiliza los costos fijos ni todos los costos variables.
Para Margen Bruto solo usamos los costos directos de producir lo que se vendió, también conocidos como COGS (Cost of Goods Sold).
Piensa en COGS como “el costo directo por unidad vendida”, no como el costo total de la empresa.
Margen Bruto: conectando ingresos con costos directos (COGS)
El Margen Bruto mide qué tan rentable es el producto en sí, antes de considerar otros costos operativos como marketing, administración, logística, etc.
Usamos únicamente los costos directos de producir lo que vendimos.
Estos costos directos se conocen como COGS (Cost of Goods Sold).
¿Qué es exactamente COGS?
COGS incluye únicamente los costos necesarios para que el producto exista y pueda ser vendido. No incluye marketing, administración ni gastos fijos generales de la empresa.
• Materia prima
• Mano de obra directa
• Costo de adquisición del inventario
• Ensamble, empaque directo
• Costos directos por unidad vendida
• Marketing y publicidad
• Sueldos administrativos
• Alquiler de oficinas
• Servicios generales
• Tecnología, contabilidad, impuestos
En resumen:
COGS se enfoca solo en los costos directos.
No representa todos los costos de la empresa.
Utilidad Bruta = Ingresos − COGS
Margen Bruto (%) = (Utilidad Bruta / Ingresos) × 100
Ejemplo simple
- Ingresos: 10,000 USD
- COGS: 6,000 USD (solo costos directos)
Utilidad Bruta:
10,000 − 6,000 = 4,000 USD
Margen Bruto (%):
(4,000 ÷ 10,000) × 100 = 40%
Un margen bruto del 40% significa que por cada 1 USD vendido, la empresa obtiene 0.40 USD después de cubrir los costos directos.
Este margen es clave para entender si un producto es rentable y cuánto “aire” queda para cubrir el resto de los gastos (operativos, administrativos, financieros, etc.).
1. ROI (Return on Investment)
El ROI mide qué tan rentable fue una inversión respecto al dinero que se invirtió. Es una métrica clave para decidir si un proyecto, campaña o iniciativa valió la pena.
ROI = (Ganancia de la inversión − Monto invertido) ÷ Monto invertidoROI (%) = ROI × 100
Ejemplo:
- Inversión en campaña de marketing: 5 000 USD
- Ingresos generados por esa campaña: 9 000 USD
Paso 1. Calcular la ganancia de la inversión:
Ganancia = 9 000 − 5 000 = 4 000 USD
Paso 2. Calcular ROI:
ROI = 4 000 ÷ 5 000 = 0.8
ROI (%) = 0.8 × 100 = 80 %
Un ROI de 80 % significa que por cada 1 USD invertido la empresa ganó 0.80 USD adicionales.
3. Análisis de tendencias y comparaciones año a año (YoY)
El análisis de tendencias permite ver cómo evolucionan los KPIs a lo largo del tiempo (mes a mes, trimestre a trimestre, año a año).
3.1 Crecimiento porcentual entre periodos
Se usa para ver cuánto creció o decreció un KPI respecto a un periodo anterior.
Crecimiento (%) = (Valor actual − Valor anterior) ÷ Valor anterior × 100
Ejemplo (ingresos mensuales):
| Mes | Ingresos (USD) |
|---|---|
| Enero | 20 000 |
| Febrero | 24 000 |
Cálculo del crecimiento de Febrero vs Enero:
Crecimiento (%) = (24 000 − 20 000) ÷ 20 000 × 100 = 20 %
Los ingresos crecieron 20 % de enero a febrero.
3.2 Comparación año a año (YoY: Year over Year)
La comparación YoY contrasta el mismo periodo en años distintos (por ejemplo, ingresos de Q1 2024 vs Q1 2023) para ver si el negocio está creciendo o no.
YoY (%) = (Valor actual − Valor mismo periodo año anterior) ÷ Valor año anterior × 100
Ejemplo (ingresos anuales):
| Año | Ingresos (USD) |
|---|---|
| 2023 | 200 000 |
| 2024 | 230 000 |
Cálculo YoY:
YoY (%) = (230 000 − 200 000) ÷ 200 000 × 100 = 15 %
El negocio creció un 15 % en ingresos respecto al año anterior. Este tipo de métrica es clave para evaluar la salud y el crecimiento de la empresa en el tiempo.
3.3 Conexión entre KPIs
En la práctica, un analista no mira cada KPI aislado, sino en conjunto:
- Los ingresos muestran tamaño del negocio.
- Los costos y el margen bruto muestran eficiencia operativa.
- El ROI muestra si las inversiones específicas están funcionando.
- Las tendencias y YoY muestran si el negocio mejora, se estanca o empeora.
Afianzando conceptos clave (con respuestas ocultas)
Usa esta sección para comprobar tu comprensión sobre Ingresos, COGS, Costos Totales, Margen Bruto, ROI y análisis de tendencias. Haz clic en los botones para mostrar las respuestas.
1. Preguntas conceptuales
-
¿Qué diferencia existe entre COGS y Costos Totales?
Piensa en costos directos vs costos de operación.Ver respuesta
COGS incluye solo los costos directos de producir lo que se vendió (materia prima, mano de obra directa, costo de inventario, etc.).
Los Costos Totales incluyen COGS más todos los gastos operativos: administración, marketing, alquiler, tecnología, etc. -
¿Por qué el Margen Bruto no utiliza costos fijos ni todos los costos variables?
Ver respuesta
Porque el Margen Bruto mide la rentabilidad del producto, no de toda la empresa. Por eso solo descuenta los costos directos (COGS) asociados a producir lo que se vendió, y deja fuera los costos operativos generales (fijos o variables) como marketing, administración, etc. -
¿Qué significa que un producto tenga un margen bruto del 60%?
Ver respuesta
Que por cada 1 unidad de moneda vendida (por ejemplo, 1 USD), la empresa se queda con 0.60 después de cubrir los costos directos de producción (COGS). Ese 60% es lo que queda disponible para cubrir otros gastos y generar utilidad neta. -
¿El ROI analiza la rentabilidad de un producto o de una inversión?
Ver respuesta
El ROI analiza la rentabilidad de una inversión específica (una campaña, un proyecto, una iniciativa), comparando cuánto se ganó respecto a lo que se invirtió. -
¿Qué información aporta comparar ingresos mes a mes (MoM) vs año a año (YoY)?
Ver respuesta
MoM (Month over Month) muestra variaciones de corto plazo entre meses consecutivos.
YoY (Year over Year) compara el mismo periodo entre años distintos (ej. enero 2024 vs enero 2023) y sirve para ver crecimiento estructural, evitando efectos estacionales.
2. Mini ejercicios
Vendiste 12 unidades de un producto cuyo costo unitario es 45 USD.
¿Cuál es el COGS?
Ver respuesta
COGS = 12 × 45 = 540 USD
Vendiste 3 productos:
• 2 unidades a 80 USD
• 1 unidad a 120 USD
¿Cuál es el total de ingresos?
Ver respuesta
Ingresos = (2 × 80) + (1 × 120) = 160 + 120 = 280 USD
Ingresos: 5,000 USD
COGS: 3,000 USD
¿Cuál es la utilidad bruta y el margen bruto (%)?
Ver respuesta
5,000 − 3,000 = 2,000 USDMargen Bruto (%) =
(2,000 ÷ 5,000) × 100 = 40%
Inversión en campaña: 2,000 USD
Ingresos obtenidos: 5,500 USD
¿Cuál es el ROI (%)?
Ver respuesta
5,500 − 2,000 = 3,500 USDROI =
3,500 ÷ 2,000 = 1.75 → 175 %(Por cada 1 USD invertido se ganaron 1.75 USD adicionales).
3. Verdadero / Falso
Intenta responder primero y luego despliega las soluciones.
| Enunciado | Tu respuesta | Solución |
|---|---|---|
| COGS incluye costos de marketing. | V / F |
VerFalso. El marketing es un gasto operativo, no un costo directo del producto. |
| El margen bruto es un indicador de rentabilidad del producto. | V / F |
VerVerdadero. Mide cuánta ganancia deja el producto después de cubrir los costos directos (COGS). |
| El ROI mide cuánto ganó una empresa respecto a una inversión puntual. | V / F |
VerVerdadero. El ROI se centra en una inversión específica (campaña, proyecto, etc.). |
| Los costos fijos cambian según cuántas unidades se produzcan. | V / F |
VerFalso. Los costos fijos no dependen directamente del volumen producido (ej. alquiler, sueldos administrativos). |
| El análisis YoY compara periodos del mismo mes o trimestre entre años. | V / F |
VerVerdadero. YoY compara el mismo periodo en años diferentes para evaluar crecimiento real. |
4. Completa los espacios
-
Ingresos se calculan como:
________ × ________.Ver respuesta
Ingresos = Precio de venta × Cantidad vendida -
COGS representa los costos __________ asociados a producir lo vendido.
Ver respuesta
directos. Costos directos de producción o adquisición del producto. -
Margen Bruto = (Ingresos − ________) ÷ ________ × 100.
Ver respuesta
Margen Bruto (%) = (Ingresos − COGS) ÷ Ingresos × 100 -
El ROI compara la ganancia obtenida con el monto _________.
Ver respuesta
invertido. -
Un análisis YoY compara el mismo periodo entre __________ distintos.
Ver respuesta
años.
5. Mini reto final
• Ingresos totales: 18,400 USD
• COGS total: 11,200 USD
• Inversión en campaña: 2,500 USD
Pregunta 1: ¿Cuál es el margen bruto (%)?
Pregunta 2: ¿Cuál es el ROI de la campaña?
Tip: primero calcula la utilidad bruta, luego aplica las fórmulas.
Ver respuestas
Utilidad Bruta =
18,400 − 11,200 = 7,200 USDMargen Bruto (%) =
(7,200 ÷ 18,400) × 100 ≈ 39.1 %2) ROI de la campaña
Si usamos la definición básica de ROI vista en clase (ingresos asociados − inversión) / inversión:
Ganancia de la inversión =
18,400 − 2,500 = 15,900 USDROI ≈
15,900 ÷ 2,500 = 6.36 → 636 %Nota: en un análisis más avanzado podríamos usar la utilidad bruta en lugar de los ingresos para un ROI “más conservador”. En ese caso:
Ganancia =
7,200 − 2,500 = 4,700 → ROI ≈ 4,700 ÷ 2,500 = 188 %.
Mini Base de Datos Relacional para Practicar KPIs de Negocio
Esta base de datos está creda para practicar consultas SQL enfocadas en KPIs clave como Ingresos, Costos, Margen Bruto, ROI y Análisis de Tendencias.
- Consultas con JOIN, GROUP BY, filtros y agregaciones.
- Cálculo de Ingresos, Costos, Margen Bruto.
- Cálculo de ROI por campaña o proyecto.
- Análisis de tendencias mensuales y comparaciones año a año (YoY).
1. Esquema general (tablas y relaciones)
customers– información de clientes.products– catálogo de productos, con precio y costo unitario.orders– encabezado de pedidos (fecha, cliente, campaña).order_items– detalle de productos vendidos en cada pedido.campaigns– campañas de marketing e inversión asociada (para ROI).

Relaciones clave (modelo estrella sencillo):
customers (1) ──── (N) orders (1) ──── (N) order_items (N) ──── (1) productscampaigns (1) ──── (N) orders
2. Tablas y campos
2.1 Tabla customers
Información básica de clientes para segmentar y analizar por país o tipo de cliente.
| Campo | Tipo | Descripción |
|---|---|---|
customer_id |
INT (PK) | Identificador único del cliente. |
customer_name |
VARCHAR | Nombre del cliente. |
country |
VARCHAR | País de residencia. |
segment |
VARCHAR | Segmento (e.g. Retail, Enterprise, Online). |
2.2 Tabla products
Define productos, precios de venta y costos unitarios para calcular margen bruto.
| Campo | Tipo | Descripción |
|---|---|---|
product_id |
INT (PK) | Identificador único del producto. |
product_name |
VARCHAR | Nombre del producto. |
category |
VARCHAR | Categoría (e.g. Software, Hardware, Servicio). |
unit_price |
DECIMAL | Precio de venta por unidad. |
unit_cost |
DECIMAL | Costo de producción por unidad (para COGS). |
2.3 Tabla campaigns
Campañas de marketing o proyectos de inversión para calcular ROI.
| Campo | Tipo | Descripción |
|---|---|---|
campaign_id |
INT (PK) | Identificador único de la campaña. |
campaign_name |
VARCHAR | Nombre de la campaña. |
start_date |
DATE | Fecha de inicio. |
end_date |
DATE | Fecha de cierre. |
channel |
VARCHAR | Canal (Email, Ads, Social Media, etc.). |
investment_amount |
DECIMAL | Monto invertido en la campaña (para ROI). |
2.4 Tabla orders
Encabezado de cada pedido / orden de venta.
| Campo | Tipo | Descripción |
|---|---|---|
order_id |
INT (PK) | Identificador único del pedido. |
order_date |
DATE | Fecha del pedido (sirve para tendencias y YoY). |
customer_id |
INT (FK → customers) | Cliente que realizó la compra. |
campaign_id |
INT (FK → campaigns, NULL) | Campaña que originó la venta (si aplica). |
2.5 Tabla order_items
Detalle de líneas de cada pedido (productos, cantidades, precios). Aquí se calculan ingresos y costos.
| Campo | Tipo | Descripción |
|---|---|---|
order_item_id |
INT (PK) | Identificador único de la línea del pedido. |
order_id |
INT (FK → orders) | Pedido al que pertenece la línea. |
product_id |
INT (FK → products) | Producto vendido. |
quantity |
INT | Cantidad vendida. |
unit_price |
DECIMAL | Precio de venta aplicado (permite simular descuentos). |
Ejercicios Demostrativos JOIN SQL - KPIs de Negocio#
# 1. Ingresos totales de la empresa
# Hacemos la conexión a la Database
conn = sqlite3.connect("Marketing_salesDb.db")
query = '''
SELECT
SUM(unit_price * quantity) AS total_revenue
FROM order_items;
'''
pd.read_sql(query, con=conn)
| total_revenue | |
|---|---|
| 0 | 472440 |
# 2. COGS total (costo directo total)
query = '''
SELECT
SUM(p.unit_cost * oi.quantity) AS total_cogs
FROM order_items AS oi
JOIN products AS p
ON oi.product_id = p.product_id;
'''
pd.read_sql(query, con=conn)
| total_cogs | |
|---|---|
| 0 | 190380 |
# 3. Margen bruto total (%)
query = '''
SELECT
SUM(oi.unit_price * oi.quantity) AS total_revenue,
SUM(p.unit_cost * oi.quantity) AS total_cogs,
SUM(oi.unit_price * oi.quantity)
- SUM(p.unit_cost * oi.quantity) AS gross_profit,
(
(SUM(oi.unit_price * oi.quantity)
- SUM(p.unit_cost * oi.quantity))
/ SUM(oi.unit_price * oi.quantity) * 100
) AS gross_margin_pct
FROM order_items AS oi
JOIN products AS p
ON oi.product_id = p.product_id;
'''
pd.read_sql(query, con=conn)
| total_revenue | total_cogs | gross_profit | gross_margin_pct | |
|---|---|---|---|---|
| 0 | 472440 | 190380 | 282060 | 0 |
# 4. Ingresos por mes (año + mes)
query = '''
SELECT
strftime('%Y', o.order_date) AS year,
strftime('%m', o.order_date) AS month,
SUM(oi.unit_price * oi.quantity) AS monthly_revenue
FROM orders AS o
JOIN order_items AS oi
ON o.order_id = oi.order_id
GROUP BY
strftime('%Y', o.order_date),
strftime('%m', o.order_date)
ORDER BY
strftime('%Y', o.order_date),
strftime('%m', o.order_date);
'''
pd.read_sql(query, con=conn)
| year | month | monthly_revenue | |
|---|---|---|---|
| 0 | 2024 | 01 | 32605 |
| 1 | 2024 | 02 | 6640 |
| 2 | 2024 | 03 | 32300 |
| 3 | 2024 | 04 | 38400 |
| 4 | 2024 | 05 | 3900 |
| 5 | 2024 | 06 | 24125 |
| 6 | 2024 | 07 | 34880 |
| 7 | 2024 | 08 | 63400 |
| 8 | 2024 | 09 | 30150 |
| 9 | 2024 | 10 | 2810 |
| 10 | 2024 | 11 | 6500 |
| 11 | 2024 | 12 | 32610 |
| 12 | 2025 | 01 | 38300 |
| 13 | 2025 | 02 | 3850 |
| 14 | 2025 | 03 | 40400 |
| 15 | 2025 | 04 | 18620 |
| 16 | 2025 | 05 | 62950 |
Sesión práctica: SQL + JOINS + KPIs de Negocio - Ingresos, COGS, Margen Bruto, ROI y Análisis de Tendencias#
# 6. Top 5 clientes por ingresos
query = '''
SELECT
c.customer_id,
c.customer_name,
c.country,
SUM(oi.unit_price * oi.quantity) AS total_revenue
FROM customers AS c
JOIN orders AS o
ON c.customer_id = o.customer_id
JOIN order_items AS oi
ON o.order_id = oi.order_id
GROUP BY
c.customer_id,
c.customer_name,
c.country
ORDER BY
total_revenue DESC
LIMIT 5;
'''
pd.read_sql(query, con=conn)
| customer_id | customer_name | country | total_revenue | |
|---|---|---|---|---|
| 0 | 5 | Pedro Ruiz | Argentina | 101350 |
| 1 | 10 | Gabriela Ortiz | España | 63400 |
| 2 | 4 | Ana Torres | España | 50920 |
| 3 | 15 | Daniel Cuevas | Colombia | 38300 |
| 4 | 1 | Juan Pérez | Colombia | 34005 |
# 7. Margen bruto por cliente (%)
query = '''
SELECT
c.customer_id,
c.customer_name,
c.country,
SUM(oi.unit_price * oi.quantity) AS revenue,
SUM(p.unit_cost * oi.quantity) AS cogs,
SUM(oi.unit_price * oi.quantity)
- SUM(p.unit_cost * oi.quantity) AS gross_profit,
(
(SUM(oi.unit_price * oi.quantity)
- SUM(p.unit_cost * oi.quantity))
/ SUM(oi.unit_price * oi.quantity) * 100
) AS gross_margin_pct
FROM customers AS c
JOIN orders AS o
ON c.customer_id = o.customer_id
JOIN order_items AS oi
ON o.order_id = oi.order_id
JOIN products AS p
ON oi.product_id = p.product_id
GROUP BY
c.customer_id,
c.customer_name,
c.country
ORDER BY
gross_margin_pct DESC;
'''
pd.read_sql(query, con=conn)
| customer_id | customer_name | country | revenue | cogs | gross_profit | gross_margin_pct | |
|---|---|---|---|---|---|---|---|
| 0 | 1 | Juan Pérez | Colombia | 34005 | 21605 | 12400 | 0 |
| 1 | 2 | María Gómez | México | 31540 | 19625 | 11915 | 0 |
| 2 | 3 | Luis Hernández | USA | 17950 | 4400 | 13550 | 0 |
| 3 | 4 | Ana Torres | España | 50920 | 30385 | 20535 | 0 |
| 4 | 5 | Pedro Ruiz | Argentina | 101350 | 19650 | 81700 | 0 |
| 5 | 6 | Sofía Delgado | Chile | 3900 | 1560 | 2340 | 0 |
| 6 | 7 | Carlos Silva | Brasil | 24125 | 15350 | 8775 | 0 |
| 7 | 8 | Julia Castro | Colombia | 16400 | 3840 | 12560 | 0 |
| 8 | 9 | Diego López | USA | 18480 | 10270 | 8210 | 0 |
| 9 | 10 | Gabriela Ortiz | España | 63400 | 12650 | 50750 | 0 |
| 10 | 11 | Ricardo Molina | México | 30150 | 20060 | 10090 | 0 |
| 11 | 12 | Laura Castillo | Chile | 2810 | 1500 | 1310 | 0 |
| 12 | 13 | Samuel Méndez | Argentina | 6500 | 2725 | 3775 | 0 |
| 13 | 14 | Paula Salinas | Brasil | 32610 | 20370 | 12240 | 0 |
| 14 | 15 | Daniel Cuevas | Colombia | 38300 | 6390 | 31910 | 0 |
# 8. Ingresos por país (top 5)
query = '''
SELECT
c.country,
SUM(oi.unit_price * oi.quantity) AS revenue_by_country
FROM customers AS c
JOIN orders AS o
ON c.customer_id = o.customer_id
JOIN order_items AS oi
ON o.order_id = oi.order_id
GROUP BY
c.country
ORDER BY
revenue_by_country DESC
LIMIT 5;
'''
pd.read_sql(query, con=conn)
| country | revenue_by_country | |
|---|---|---|
| 0 | España | 114320 |
| 1 | Argentina | 107850 |
| 2 | Colombia | 88705 |
| 3 | México | 61690 |
| 4 | Brasil | 56735 |
# 9. Margen bruto por categoría de producto
query = '''
SELECT
p.category,
SUM(oi.unit_price * oi.quantity) AS revenue,
SUM(p.unit_cost * oi.quantity) AS cogs,
(SUM(oi.unit_price * oi.quantity) - SUM(p.unit_cost * oi.quantity))
AS gross_profit,
ROUND(
(SUM(oi.unit_price * oi.quantity) - SUM(p.unit_cost * oi.quantity))
* 1.0 / NULLIF(SUM(oi.unit_price * oi.quantity), 0) * 100
, 2) AS gross_margin_pct
FROM products AS p
JOIN order_items AS oi
ON p.product_id = oi.product_id
JOIN orders AS o
ON oi.order_id = o.order_id
GROUP BY
p.category
ORDER BY
gross_margin_pct DESC;
'''
pd.read_sql(query, con=conn)
| category | revenue | cogs | gross_profit | gross_margin_pct | |
|---|---|---|---|---|---|
| 0 | Software | 204300 | 38300 | 166000 | 81.25 |
| 1 | Servicios | 30400 | 6080 | 24320 | 80.00 |
| 2 | Accesorios | 23080 | 9700 | 13380 | 57.97 |
| 3 | Hardware | 214660 | 136300 | 78360 | 36.50 |
# 10. Ingresos atribuibles a cada campaña
query = '''
SELECT
p.category,
SUM(oi.unit_price * oi.quantity) AS revenue,
SUM(p.unit_cost * oi.quantity) AS cogs,
(SUM(oi.unit_price * oi.quantity) - SUM(p.unit_cost * oi.quantity))
AS gross_profit,
ROUND(
(SUM(oi.unit_price * oi.quantity) - SUM(p.unit_cost * oi.quantity))
* 1.0 / NULLIF(SUM(oi.unit_price * oi.quantity), 0) * 100
, 2) AS gross_margin_pct
FROM products AS p
JOIN order_items AS oi
ON p.product_id = oi.product_id
JOIN orders AS o
ON oi.order_id = o.order_id
GROUP BY
p.category
ORDER BY
gross_margin_pct DESC;
'''
pd.read_sql(query, con=conn)
| category | revenue | cogs | gross_profit | gross_margin_pct | |
|---|---|---|---|---|---|
| 0 | Software | 204300 | 38300 | 166000 | 81.25 |
| 1 | Servicios | 30400 | 6080 | 24320 | 80.00 |
| 2 | Accesorios | 23080 | 9700 | 13380 | 57.97 |
| 3 | Hardware | 214660 | 136300 | 78360 | 36.50 |
# 11. ROI por campaña
query = '''
SELECT
c.campaign_id,
c.campaign_name,
c.channel,
c.investment_amount,
SUM(oi.unit_price * oi.quantity) AS revenue_from_campaign,
(SUM(oi.unit_price * oi.quantity) - c.investment_amount) AS profit_from_campaign,
((SUM(oi.unit_price * oi.quantity) - c.investment_amount)
/ c.investment_amount * 100) AS roi_pct
FROM campaigns AS c
LEFT JOIN orders AS o
ON c.campaign_id = o.campaign_id
LEFT JOIN order_items AS oi
ON o.order_id = oi.order_id
GROUP BY
c.campaign_id,
c.campaign_name,
c.channel,
c.investment_amount
ORDER BY
roi_pct DESC;
'''
pd.read_sql(query, con=conn)
| campaign_id | campaign_name | channel | investment_amount | revenue_from_campaign | profit_from_campaign | roi_pct | |
|---|---|---|---|---|---|---|---|
| 0 | 3 | Campaña Back to School | Social Media | 5000 | 98280 | 93280 | 1800 |
| 1 | 2 | Campaña Navidad | 7000 | 70910 | 63910 | 900 | |
| 2 | 4 | Campaña Primavera | Ads | 6000 | 40400 | 34400 | 500 |
| 3 | 5 | Campaña Verano | SEO | 4000 | 24125 | 20125 | 500 |
| 4 | 1 | Campaña Black Friday | Ads | 10000 | 9310 | -690 | 0 |
# 12. Comparación de ingresos por año (2024 vs 2025)
query = '''
SELECT
strftime('%Y', o.order_date) AS year,
SUM(oi.unit_price * oi.quantity) AS revenue
FROM orders AS o
JOIN order_items AS oi
ON o.order_id = oi.order_id
GROUP BY
strftime('%Y', o.order_date)
ORDER BY
year;
'''
pd.read_sql(query, con=conn)
| year | revenue | |
|---|---|---|
| 0 | 2024 | 308320 |
| 1 | 2025 | 164120 |
# 13b. Ticket promedio por cliente
query = '''
SELECT
c.customer_id,
c.customer_name,
SUM(oi.unit_price * oi.quantity) AS total_revenue,
COUNT(DISTINCT o.order_id) AS num_orders,
SUM(oi.unit_price * oi.quantity)
/ COUNT(DISTINCT o.order_id) AS avg_order_value_per_customer
FROM customers AS c
JOIN orders AS o
ON c.customer_id = o.customer_id
JOIN order_items AS oi
ON o.order_id = oi.order_id
GROUP BY
c.customer_id,
c.customer_name
ORDER BY
avg_order_value_per_customer DESC;
'''
pd.read_sql(query, con=conn)
| customer_id | customer_name | total_revenue | num_orders | avg_order_value_per_customer | |
|---|---|---|---|---|---|
| 0 | 10 | Gabriela Ortiz | 63400 | 1 | 63400 |
| 1 | 5 | Pedro Ruiz | 101350 | 2 | 50675 |
| 2 | 15 | Daniel Cuevas | 38300 | 1 | 38300 |
| 3 | 14 | Paula Salinas | 32610 | 1 | 32610 |
| 4 | 11 | Ricardo Molina | 30150 | 1 | 30150 |
| 5 | 4 | Ana Torres | 50920 | 2 | 25460 |
| 6 | 7 | Carlos Silva | 24125 | 1 | 24125 |
| 7 | 9 | Diego López | 18480 | 1 | 18480 |
| 8 | 1 | Juan Pérez | 34005 | 2 | 17002 |
| 9 | 8 | Julia Castro | 16400 | 1 | 16400 |
| 10 | 2 | María Gómez | 31540 | 2 | 15770 |
| 11 | 3 | Luis Hernández | 17950 | 2 | 8975 |
| 12 | 13 | Samuel Méndez | 6500 | 1 | 6500 |
| 13 | 6 | Sofía Delgado | 3900 | 1 | 3900 |
| 14 | 12 | Laura Castillo | 2810 | 1 | 2810 |
# 14. Top 10 productos más vendidos (por cantidad)
query = '''
SELECT
p.product_id,
p.product_name,
p.category,
SUM(oi.quantity) AS total_quantity_sold
FROM products AS p
JOIN order_items AS oi
ON p.product_id = oi.product_id
JOIN orders AS o
ON oi.order_id = o.order_id
GROUP BY
p.product_id,
p.product_name,
p.category
ORDER BY
total_quantity_sold DESC
LIMIT 10;
'''
pd.read_sql(query, con=conn)
| product_id | product_name | category | total_quantity_sold | |
|---|---|---|---|---|
| 0 | 5 | Software CRM Pro | Software | 256 |
| 1 | 10 | Licencia Analytics | Software | 255 |
| 2 | 9 | Router WiFi 6 | Hardware | 205 |
| 3 | 4 | Monitor 27" | Hardware | 203 |
| 4 | 3 | Teclado Mecánico | Accesorios | 156 |
| 5 | 8 | Servicio de Soporte | Servicios | 152 |
| 6 | 2 | Mouse Inalámbrico | Accesorios | 112 |
| 7 | 7 | Tablet 10" | Hardware | 104 |
| 8 | 1 | Laptop Pro 15 | Hardware | 55 |
| 9 | 6 | Audífonos Pro | Accesorios | 52 |
# 15. Número de clientes por campaña
query = '''
SELECT
c.campaign_id,
c.campaign_name,
c.channel,
COUNT(DISTINCT o.customer_id) AS num_customers_acquired
FROM campaigns AS c
LEFT JOIN orders AS o
ON c.campaign_id = o.campaign_id
GROUP BY
c.campaign_id,
c.campaign_name,
c.channel
ORDER BY
num_customers_acquired DESC;
'''
pd.read_sql(query, con=conn)
| campaign_id | campaign_name | channel | num_customers_acquired | |
|---|---|---|---|---|
| 0 | 3 | Campaña Back to School | Social Media | 3 |
| 1 | 1 | Campaña Black Friday | Ads | 2 |
| 2 | 2 | Campaña Navidad | 2 | |
| 3 | 4 | Campaña Primavera | Ads | 2 |
| 4 | 5 | Campaña Verano | SEO | 1 |
Cierre#
Exporta una tabla final , genera una visualización sencilla (gráfico de barras o línea) y compártela por el chat de la sesión.
Siguientes Pasos#
Próxima sesión: Practica con tus compañeros - revisión requisitos para el proyecto del sprint 3.
Participación continua: asistir a Co-Learning y a Sprint Focus, y usar los canales de Discord para hacer preguntas.
Recordatorios: la grabación y recursos utilizados, se comparten al finalizar la sesión; en caso de necesitar apoyo adicional, agenda un 1:1.