https://github.com/johnny-godoy/laboratorios-mds/blob/main/lab%205/laboratorio_5.ipynb¶numpy con respecto a trabajar en Python 'puro'.for puede afectar en la eficiencia en al procesar datos masivos.El laboratorio deberá ser desarrollado sin el uso indiscriminado de iteradores nativos de python (aka "for", "while"). La idea es que aprendan a exprimir al máximo las funciones optimizadas que nos entrega numpy, las cuales vale mencionar, son bastante más eficientes que los iteradores nativos sobre arreglos (o tensores).
import datetime
from IPython.display import display, Markdown, Latex
import matplotlib.pyplot as plt
import missingno as msno
import numpy as np
from pandas.api.types import is_numeric_dtype
from pandas.core.dtypes.common import is_datetime_or_timedelta_dtype
import pandas as pd
import plotly.express as px
from scipy import stats
Mr. Lepin Mora quien es el gerente de una cotizada tienda de retail en Europa, les solicita si pueden analizar los datos de algunas de sus tiendas y si es posible extraer los diferenciar los tipos de clientes que posee el retail.
Para esto, el área de ventas les entrega el archivo online_retail_II.xlsx con el que se les pide que cargue y visualicen algunas de las filas que componen el Dataset.
Realice una primera visualización de los datos y señale los atributos que componen el dataset. Señale las columnas que conforman el dataset, el tipo de variable presente en cada columna y comente que representa cada una de estas.
Respuesta:
df_retail = pd.read_pickle("data/online_retail_II.pickle")
df_retail = df_retail.astype(
{
"Invoice": "category",
"StockCode": "category",
"Description": str,
"Customer ID": "category",
}
)
df_retail.head()
| Invoice | StockCode | Description | Quantity | InvoiceDate | Price | Customer ID | Country | |
|---|---|---|---|---|---|---|---|---|
| 0 | 489434 | 85048 | 15CM CHRISTMAS GLASS BALL 20 LIGHTS | 12 | 2009-12-01 07:45:00 | 6.95 | 13085.0 | United Kingdom |
| 1 | 489434 | 79323P | PINK CHERRY LIGHTS | 12 | 2009-12-01 07:45:00 | 6.75 | 13085.0 | United Kingdom |
| 2 | 489434 | 79323W | WHITE CHERRY LIGHTS | 12 | 2009-12-01 07:45:00 | 6.75 | 13085.0 | United Kingdom |
| 3 | 489434 | 22041 | RECORD FRAME 7" SINGLE SIZE | 48 | 2009-12-01 07:45:00 | 2.10 | 13085.0 | United Kingdom |
| 4 | 489434 | 21232 | STRAWBERRY CERAMIC TRINKET BOX | 24 | 2009-12-01 07:45:00 | 1.25 | 13085.0 | United Kingdom |
df_no_duplicates_or_nans = df_retail.drop_duplicates().dropna()
df_no_duplicates_or_nans.groupby(["Invoice", "StockCode"]).size()
Invoice StockCode
489434 10002 0
10080 0
10109 0
10120 0
10125 0
..
C538164 gift_0001_60 0
gift_0001_70 0
gift_0001_80 0
gift_0001_90 0
m 0
Length: 133475712, dtype: int64
¿Cuáles son los tipos de datos?
df_retail.dtypes
Invoice category StockCode category Description object Quantity int64 InvoiceDate datetime64[ns] Price float64 Customer ID category Country object dtype: object
¿Qué rango tienen los datos?
for nombre, columna in df_retail.select_dtypes(["category", "object"]).items():
print(f"Cantidad de valores únicos de {nombre}: {columna.nunique()}")
for nombre, columna in df_retail.select_dtypes(["int64", "datetime64[ns]", "float64"]).items():
print(f"Rando de valores para {nombre}: {[columna.min(), columna.max()]}")
Cantidad de valores únicos de Invoice: 28816
Cantidad de valores únicos de StockCode: 4632
Cantidad de valores únicos de Description: 4682
Cantidad de valores únicos de Customer ID: 4383
Cantidad de valores únicos de Country: 40
Rando de valores para Quantity: [-9600, 19152]
Rando de valores para InvoiceDate: [Timestamp('2009-12-01 07:45:00'), Timestamp('2010-12-09 20:01:00')]
Rando de valores para Price: [-53594.36, 25111.09]
| Atributo | Tipo de Datos | Rango | Explicación |
|---|---|---|---|
| Invoice | category | 28816 códigos | Identificador de la boleta |
| StockCode | category | 4632 códigos | Código del producto |
| Description | object | 4682 descripciones | Descripción del producto |
| Quantity | int64 | [-9600, 19152] | Cantidad transaccionada |
| InvoiceDate | datetime64[ns] | 01/12/2009 - 09/12/2010 | Fecha de la transacción |
| Price | float64 | [-53594.36, 25111.09] | Precio del producto |
| Customer ID | category | 4383 códigos | Identificador del cliente |
| Country | object | 40 países | País de la transacción |
En base a la primera visualización del dataset, Don Mora le solicita que realicen un análisis exploratorio de los datos, para esto les deberán realizar un análisis univariado y multivariado. De la revisión, ustedes deben explicar potenciales anomalías visualizadas y señalar si existe la necesidad de realizar una limpieza de datos.
Explique a que nos referimos con análisis univariable, multivariable y de datos faltantes. ¿Qué beneficios nos otorga estudiar estos datos?. Sea conciso con su respuesta y no escriba mas de 5 líneas para su respuesta.
Respuesta a la Pregunta:
El análisis univariado estudia la distribución marginal de cada una de las columnas por separado. En el análisis multivariado, uno estudia las relaciones entre múltiples variables. Analizar datos faltantes busca ver si hay columnas incompletas (valores NaN u otros) o filas faltantes (eg: en caso de datos secuenciales, ver si falta un paso).
A continuación, se le presentan dos funciones para analizar los datos que componen un dataframe. La primera de estas es la función profile_serie() la cual recibe una serie y le entrega un análisis detallado de los datos que conforman dicha serie.
Ejecute la funcion profile_serie() sobre cada serie para realizar un análisis univariado de estas. A continuación, comente acerca de el comportamiento de cada variable según las estadísticas descriptivas y los gráficos generados.
def profile_serie(serie_in, n_samples=1000, random_state=42):
serie = serie_in.copy()
profile = pd.Series(dtype='object')
profile["Type"] = serie.dtype
profile = pd.concat([profile, serie.describe(datetime_is_numeric=True)])
# profile = pd.Series([])
if is_numeric_dtype(serie):
profile["Negative"] = (serie < 0).sum()
profile["Negative (%)"] = (
str(round((serie < 0).sum() / len(serie) * 100, 2)) + " %"
)
profile["Zeros"] = (serie == 0).sum()
profile["Zeros (%)"] = (
str(round((serie == 0).sum() / len(serie) * 100, 2)) + " %"
)
profile["Kurt"] = serie.kurt()
profile["Skew"] = serie.skew()
profile[" "] = " " # espacio
profile["Missing cells"] = serie.isnull().sum()
profile["Missing cells (%)"] = (
str(round(serie.isnull().sum() / len(serie) * 100, 2)) + " %"
)
profile["Duplicate rows"] = serie.duplicated(False).sum()
profile["Duplicate rows (%)"] = (
str(round(serie.duplicated(False).sum() / len(serie) * 100, 2)) + " %"
)
profile["Total size in memory"] = str(serie.memory_usage(index=True)) + " bytes"
# profile = pd.concat([profile, description])
profile = profile.rename(
index={
"count": "Number of observations",
"mean": "Mean",
"std": "Std",
"min": "Min",
"max": "Max",
"unique": "Unique",
"top": "Top",
"freq": "Freq",
}
)
no_outliers_fig = None
if is_numeric_dtype(serie):
sampled_serie = serie.sample(n_samples, random_state=random_state)
fig = px.histogram(
sampled_serie, marginal="box", title=f"{serie.name} - With Outliers"
)
no_outliers = sampled_serie.loc[(np.abs(stats.zscore(sampled_serie)) < 3)]
# zscore = https://es.wikipedia.org/wiki/Unidad_tipificada
no_outliers_fig = px.histogram(
no_outliers, marginal="box", title=f"{serie.name} - Without Outliers"
)
elif is_datetime_or_timedelta_dtype(serie):
sampled_serie = serie.sample(n_samples, random_state=random_state)
fig = px.histogram(sampled_serie, marginal="box", title=f"{serie.name}")
else:
count = (
serie.value_counts()[0:100]
.reset_index()
.rename(columns={"index": serie.name, serie.name: "Count"})
)
fig = px.bar(
x=count[serie.name].astype(str),
y=count["Count"],
title=f"100 Most common categories of {serie.name}",
)
display(Markdown(f'## {serie.name} Profile'))
display(profile)
fig.show()
if no_outliers_fig:
no_outliers_fig.show()
# return fig, profile
Análisis de la primera serie...
profile_serie(df_retail['Invoice'])
Type category
Number of observations 525461
Unique 28816
Top 537434
Freq 675
Missing cells 0
Missing cells (%) 0.0 %
Duplicate rows 517456
Duplicate rows (%) 98.48 %
Total size in memory 2338386 bytes
dtype: object
Como ya sabíamos, este es un identificador, por lo cual existen muchas categorías. Estas no tienen orden asociado.
Estas se repiten dado que en una boleta se pueden realizar varias compras, así que hay una gran cantidad de filas repetidas. Como es necesario que toda transacción tenga boleta asociada, no hay valores faltanres.
profile_serie(df_retail.StockCode)
Type category
Number of observations 525461
Unique 4632
Top 85123A
Freq 3516
Missing cells 0
Missing cells (%) 0.0 %
Duplicate rows 525026
Duplicate rows (%) 99.92 %
Total size in memory 1220242 bytes
dtype: object
Acá el análisis es muy similar al código de boleta, pero existen muchos menos valores únicos, dado que hay menos productos posibles que boletas emitidas - así, esta tiene más valores duplicados.
profile_serie(df_retail.Description)
Type object
Number of observations 525461
Unique 4682
Top WHITE HANGING HEART T-LIGHT HOLDER
Freq 3549
Missing cells 0
Missing cells (%) 0.0 %
Duplicate rows 525198
Duplicate rows (%) 99.95 %
Total size in memory 4203816 bytes
dtype: object
Nuevamente una serie categórica con alto rango de posibilidades. De hecho, se espera que este sea muy similar a los códigos (pues dos productos deberían tener la misma despcrición si y solamente si tienen el mismo código). El histograma se distorsiona debido a las etiquetas del eje x tan largas, pero la forma es similar a los casos anteriores. Tiene mayor costo de memoria que el caso anterior pues tiene un campo de texto libre. Hay 0.3% más valores duplicados que el stockcode, y tiene 50 valores únicos extra. Cabe investigar esto más a fondo.
profile_serie(df_retail.Quantity)
Type int64
Number of observations 525461.0
Mean 10.337667
Std 107.42411
Min -9600.0
25% 1.0
50% 3.0
75% 10.0
Max 19152.0
Negative 12326
Negative (%) 2.35 %
Zeros 0
Zeros (%) 0.0 %
Kurt 6277.666908
Skew 36.044617
Missing cells 0
Missing cells (%) 0.0 %
Duplicate rows 525122
Duplicate rows (%) 99.94 %
Total size in memory 4203816 bytes
dtype: object
La distribución de la cantidad se caracteriza por tener:
La eliminación de outliers mantiene estas propiedades. Siguen existiendo valores negativos, que hay que poder interpretar dado que forman 2.35% de los datos. Viendo antes algunas de las filas, estas podían ser devoluciones.
profile_serie(df_retail.InvoiceDate)
Type datetime64[ns]
Number of observations 525461
Mean 2010-06-28 11:37:36.845017856
Min 2009-12-01 07:45:00
25% 2010-03-21 12:20:00
50% 2010-07-06 09:51:00
75% 2010-10-15 12:45:00
Max 2010-12-09 20:01:00
Missing cells 0
Missing cells (%) 0.0 %
Duplicate rows 520400
Duplicate rows (%) 99.04 %
Total size in memory 4203816 bytes
dtype: object
En el histograma notamos una distribución multimodal, que parece seguir una periodicidad, en particular, se alcanza un máximo local de datos obtenidos cada mes.
profile_serie(df_retail.Price)
Type float64
Number of observations 525461.0
Mean 4.688834
Std 146.126914
Min -53594.36
25% 1.25
50% 2.1
75% 4.21
Max 25111.09
Negative 3
Negative (%) 0.0 %
Zeros 3687
Zeros (%) 0.7 %
Kurt 64868.344873
Skew -140.768446
Missing cells 0
Missing cells (%) 0.0 %
Duplicate rows 524485
Duplicate rows (%) 99.81 %
Total size in memory 4203816 bytes
dtype: object
En los precios vemos que existen nuevamente outliers muy enormes, aunque al menos no existen precios negativos que haya que interpretar. La eliminación de outliers no fue suficientemente efectiva, pues faltaron 2 valores. Eliminando estos, la distribución se parece a una lognormal, que es razonable en montos.
profile_serie(df_retail["Customer ID"])
Type category
Number of observations 417534.0
Unique 4383.0
Top 14911.0
Freq 5710.0
Missing cells 107927
Missing cells (%) 20.54 %
Duplicate rows 525327
Duplicate rows (%) 99.97 %
Total size in memory 1218250 bytes
dtype: object
Nuevamente una columna de identificación, pero a diferencia de las anteriores, era si puede ser nula, pues no todo cliente tiene un id asignado (por ejemplo, podrían no estar registrados en el sistema). Es interesante ver en el histograma que hay clientes con muy alta cantidad de compras, lo cual rápidamente cae a ser casi constante. Esto se parece más a una ley de potencia.
profile_serie(df_retail.Country)
Type object
Number of observations 525461
Unique 40
Top United Kingdom
Freq 485852
Missing cells 0
Missing cells (%) 0.0 %
Duplicate rows 525461
Duplicate rows (%) 100.0 %
Total size in memory 4203816 bytes
dtype: object
Esencialmente todo dato es del Reino Unido.
En segundo lugar encontrará la función profile_df() que recibe un dataframe como entrada y realiza un análisis bivariado de todas las variables numéricas que conforman el dataframe, un analisis de la correlación de Pearson entre las variables numericas del dataframe y la matriz de datos faltantes.
def profile_df(dataframe_in):
df = dataframe_in.copy()
list_type = []
for col in list(df.columns):
if is_numeric_dtype(df[col]) or \
pd.core.dtypes.common.is_datetime_or_timedelta_dtype(df[col]):
list_type.append(col)
display(Markdown('## Bivariant Analysis:'))
for i in range(len(list_type)):
for j in range(i+1, len(list_type)):
plt.scatter(df[list_type[i]], df[list_type[j]])
plt.xlabel(list_type[i])
plt.ylabel(list_type[j])
plt.title(f"{list_type[i]} v/s {list_type[j]}")
plt.show()
display(Markdown('## Correlation:'))
fig_corr = px.imshow(df.corr())
fig_corr.show()
display(Markdown('## Missing Matrix:'))
fig, ax = plt.subplots(figsize=[15, 10])
msno.matrix(df, ax=ax, sparkline=False)
La matriz de valoresnulos puede ser más interesante si ordenamos los datos según fecha, para ver cómo evolucionaron los valores faltantes.
df_sorted = df_retail.sort_values("InvoiceDate")
profile_df(df_sorted)
C:\Users\David\AppData\Local\Temp\ipykernel_14496\4178638371.py:20: FutureWarning: The default value of numeric_only in DataFrame.corr is deprecated. In a future version, it will default to False. Select only valid columns or specify the value of numeric_only to silence this warning.
Respecto a los gráficos de dos variables, estos siempre muestran una recta. Esto es debido a que las variables numéricas (salvo la fecha) tienen casi sus valores extremadamente concentrados. Eliminar los valores fuera de esta recta podrían revelar patrones más interesantes. La correlación entre precio y cantidad es nula, como podría esperarse. Tal como se vió en el análisis univariado, el único caso de valores faltantes es el identificador del usuario, pues es lo único que no es necesario en una transacción válida. No se vé un patrón en el tiempo.
Como pudo ver en las secciones anteriores, los datos presentan valores erroneos, es por esto que se le solicita que realice una función que permita limpiar el dataset. Realice esta función en base observaciones propias y considere como imposible tener cantidades negativas en las ventas.
Una vez realizada la función, realice nuevamente el análisis exploratorio y comente las principales diferencias.
Respuesta:
def limpiar_datos(datos_sucios: pd.DataFrame) -> pd.DataFrame:
datos_limpios = datos_sucios.dropna().drop_duplicates() # Eliminando filas duplicadas o con nulos
# Eliminando cantidades y precios no positivos
datos_limpios = datos_limpios[datos_limpios.Quantity > 0]
datos_limpios = datos_limpios[datos_limpios.Price > 0]
return datos_limpios
df_clean = limpiar_datos(df_sorted)
df_clean.head(5)
| Invoice | StockCode | Description | Quantity | InvoiceDate | Price | Customer ID | Country | |
|---|---|---|---|---|---|---|---|---|
| 0 | 489434 | 85048 | 15CM CHRISTMAS GLASS BALL 20 LIGHTS | 12 | 2009-12-01 07:45:00 | 6.95 | 13085.0 | United Kingdom |
| 1 | 489434 | 79323P | PINK CHERRY LIGHTS | 12 | 2009-12-01 07:45:00 | 6.75 | 13085.0 | United Kingdom |
| 2 | 489434 | 79323W | WHITE CHERRY LIGHTS | 12 | 2009-12-01 07:45:00 | 6.75 | 13085.0 | United Kingdom |
| 3 | 489434 | 22041 | RECORD FRAME 7" SINGLE SIZE | 48 | 2009-12-01 07:45:00 | 2.10 | 13085.0 | United Kingdom |
| 4 | 489434 | 21232 | STRAWBERRY CERAMIC TRINKET BOX | 24 | 2009-12-01 07:45:00 | 1.25 | 13085.0 | United Kingdom |
for column in df_clean:
profile_serie(df_clean[column])
Type category
Number of observations 400916
Unique 19213
Top 500356
Freq 251
Missing cells 0
Missing cells (%) 0.0 %
Duplicate rows 399186
Duplicate rows (%) 99.57 %
Total size in memory 5296496 bytes
dtype: object
Type category
Number of observations 400916
Unique 4017
Top 85123A
Freq 3107
Missing cells 0
Missing cells (%) 0.0 %
Duplicate rows 400754
Duplicate rows (%) 99.96 %
Total size in memory 4178352 bytes
dtype: object
Type object
Number of observations 400916
Unique 4444
Top WHITE HANGING HEART T-LIGHT HOLDER
Freq 3107
Missing cells 0
Missing cells (%) 0.0 %
Duplicate rows 400710
Duplicate rows (%) 99.95 %
Total size in memory 6414656 bytes
dtype: object
Type int64
Number of observations 400916.0
Mean 13.767418
Std 97.638385
Min 1.0
25% 2.0
50% 5.0
75% 12.0
Max 19152.0
Negative 0
Negative (%) 0.0 %
Zeros 0
Zeros (%) 0.0 %
Kurt 9418.363882
Skew 79.281875
Missing cells 0
Missing cells (%) 0.0 %
Duplicate rows 400802
Duplicate rows (%) 99.97 %
Total size in memory 6414656 bytes
dtype: object
Type datetime64[ns]
Number of observations 400916
Mean 2010-07-01 05:01:16.167027712
Min 2009-12-01 07:45:00
25% 2010-03-26 13:28:00
50% 2010-07-09 10:26:00
75% 2010-10-14 13:58:45
Max 2010-12-09 20:01:00
Missing cells 0
Missing cells (%) 0.0 %
Duplicate rows 399449
Duplicate rows (%) 99.63 %
Total size in memory 6414656 bytes
dtype: object
Type float64
Number of observations 400916.0
Mean 3.305826
Std 35.047719
Min 0.001
25% 1.25
50% 1.95
75% 3.75
Max 10953.5
Negative 0
Negative (%) 0.0 %
Zeros 0
Zeros (%) 0.0 %
Kurt 62818.874688
Skew 233.142978
Missing cells 0
Missing cells (%) 0.0 %
Duplicate rows 400730
Duplicate rows (%) 99.95 %
Total size in memory 6414656 bytes
dtype: object
Type category
Number of observations 400916.0
Unique 4312.0
Top 14911.0
Freq 5568.0
Missing cells 0
Missing cells (%) 0.0 %
Duplicate rows 400825
Duplicate rows (%) 99.98 %
Total size in memory 4176360 bytes
dtype: object
Type object
Number of observations 400916
Unique 37
Top United Kingdom
Freq 364233
Missing cells 0
Missing cells (%) 0.0 %
Duplicate rows 400916
Duplicate rows (%) 100.0 %
Total size in memory 6414656 bytes
dtype: object
profile_df(df_clean)
C:\Users\David\AppData\Local\Temp\ipykernel_14496\4178638371.py:20: FutureWarning: The default value of numeric_only in DataFrame.corr is deprecated. In a future version, it will default to False. Select only valid columns or specify the value of numeric_only to silence this warning.
No hay nuevas observaciones salvo la eliminación de valores nulos. Las distribuciones numéricas siguen muy concentradas en un valor.
Sin considerar los comentarios realizados en la sección 1.2 , Don Mora les pide obtener el Top de 30 productos que generan más ganancias para la tienda de retail. Deben considerar todo el registro temporal presente en el dataset y entregar la información en un gráfico de barras de los ingresos/cantidades v/s el nombre de los productos (Utilice plotly). ¿Los artículos más vendidos son los mismos que generan más ganancias?, Comente los resultados obtenidos.
Resultados:
df_clean["Income"] = df_clean.Price*df_clean.Quantity
product_gains = df_clean.groupby("Description")[["Quantity", "Income"]].sum()
product_gains.rename(columns={"Quantity": "SoldQuantity"}, inplace=True)
product_gains.reset_index(inplace=True)
product_gains.Description = product_gains.Description.str.lower()
product_gains
| Description | SoldQuantity | Income | |
|---|---|---|---|
| 0 | doormat union jack guns and roses | 167 | 1071.25 |
| 1 | 3 stripey mice feltcraft | 662 | 1241.10 |
| 2 | 4 purple flock dinner candles | 200 | 265.20 |
| 3 | animal stickers | 385 | 80.85 |
| 4 | black pirate treasure chest | 45 | 74.25 |
| ... | ... | ... | ... |
| 4439 | zinc heart lattice tray oval | 325 | 364.15 |
| 4440 | zinc metal heart decoration | 13771 | 16472.75 |
| 4441 | zinc police box lantern | 193 | 783.70 |
| 4442 | zinc top 2 door wooden shelf | 233 | 1325.35 |
| 4443 | zinc willie winkie candle stick | 3626 | 3007.22 |
4444 rows × 3 columns
top = product_gains.nlargest(columns="SoldQuantity", n=30)[::-1]
px.bar(top, y="Description", x="SoldQuantity")
top = product_gains.nlargest(columns="Income", n=30)[::-1]
px.bar(top, y="Description", x="Income")
El artículo más vendido ("white hanging heart t-light holder") es efectivamente el que produce más ganancias, pero esto no es una regla general. "world war 2 gliders asstd designs" se vende en casi igual cantidad, pero no es de los top 30 de mayor ganancia, y "regency cakestand 3 tier" es el segundo de mayor ganancia y no está en los top 30 más vendidos.
Luego, es el segundo gráfico el que realmente no da la información de los más vendidos.
El dueño del retail en su afán por saber más sobre los datos de su firma les solicita que grafiquen las ventas respecto al tiempo. Con esto les aclara que durante el día tienen muchas variaciones en sus ventas, por lo que les recomienda que consideren el registro temporal como año-mes-día. ¿Es posible observar datos extraños?, Comente lo que observa del gráfico.
def plot_ventas(dataframe):
df_base = dataframe[["InvoiceDate", "Quantity"]].copy()
time_period = (("d", "Daily"),
("7d", "Weekly"),
("30d", "Monthly"),
("365d", "Yearly")
)
for row, (freq, freq_name) in enumerate(time_period, 1):
df_copy = df_base.copy()
df_copy.InvoiceDate = df_copy.InvoiceDate.dt.round(freq=freq)
df_grouped = df_copy.groupby("InvoiceDate").Quantity.sum().reset_index()
fig = px.bar(df_grouped, x="InvoiceDate", y="Quantity")
fig.update_layout(title_text=f"{freq_name} sales")
fig.show()
plot_ventas(df_clean)
El gráfico diario es el más interesante, donde se nota:
Esto último con excepción a algunos outliers muy fuertes, que llegan a valores como 90 mil (28 de Septiembre, 15 de Febrero).
Lo extraño de estos outliers es que solamente duran 1 día y su efecto no parece afectar a su alrededor. Si esto ocurriera, por ejemplo, por un cambio económico importante, entonces su efecto se vería reflejado en los días siguientes.
Eso ha sido todo para el lab de hoy, recuerden que el laboratorio tiene un plazo de entrega de una semana y que los días de atraso no se pueden utilizar para entregas de lab solo para tareas. Cualquier duda del laboratorio, no duden en contactarnos por mail o U-cursos.
