# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3.9.6 64-bit
#     language: python
#     name: python3
# ---

# # Análisis exploratorio de *items_ordered_2years*

# En este cuadeerno se detalla el proceso de análisis de datos de los pedidos realizados

# # Índice de Contenidos

# 1. Importación de paquetes
# 2. Carga de datos
# 3. Análisis de los datos
#
#     3.1. Variables
#
#     3.2. Duplicados
#
#     3.3. Clientes
#
#     3.4. Ventas totales
#
#     3.5. Ventas por localización
#     
# 4. Conclusiones

# ## Importación de paquetes

# Cargamos las librerías a usar.

# +
import pandas as pd
import plotly.express as px
import random

seed = 124
random.seed(seed)
# -

# ## Carga de datos

# Abrimos el fichero y lo cargamos para poder manejar la información

df_items = pd.read_csv('../Data/items_ordered_2years.txt', sep='|', on_bad_lines='skip',parse_dates=['created_at'])
df_items.sample(5, random_state=seed)

# ## Análisis de los datos

# En este apartado se analizarán todas las características de las variables

print("Dimensiones de los datos:", df_items.shape)

df_items.isna().sum()

# Existen datos perdidos o no proporcionados de 3 variables distintas

percent_missing = df_items.isnull().sum() * 100 / df_items.shape[0]
df_missing_values = pd.DataFrame({ 
    'column_name': df_items.columns,
    'percent_missing': percent_missing
})
df_missing_values.sort_values(by='percent_missing',ascending=False,inplace=True)
print(df_missing_values.to_string(index=False))

# Aunque haya datos perdidos, estos no representan una gran proporción respecto al dataset

df_items.info()

# Podemos observar que tenemos 1 variable de tipo fecha, 2 variables que son números enteros (el identificador de producto y la cantidad pedida), 3 variables son números decimales (precio base, precio de venta y porcentaje de descuento) y las demás variables que son clásificadas como tipo **object** también las podemos considerar de tipo *string*

df_items.describe()

# Al echar un vistazo por encima nos podemos dar cuenta de un par de cosas un tanto *extrañas*:
# * El mínimo precio base es negativo
# * El mínimo precio de venta es 0
# * El mínimo porcentaje de descuento es 1, por lo tanto no hay ningún pedido que no tenga aplicado un porcentaje de descuento

df_items.describe(include=object)

# Vemos que el número de pedido se repite y esto se debe a que por cada producto distinto que existen dentro de un pedido necesitamos un registro nuevo, y por eso se repide el identificador de pedido

# ### Variables

# Ahora realizaremos un análisis más específico de cada variable

# #### *num_order*

# Esta variable es el identificador de los pedidos y la podemos ver repetida ya que en un mismo pedido puede haber diferentes productos, y para cada producto distinto dentro de un mismo pedido se usan distintas entradas.

orders = df_items['num_order'].nunique()
print(f'Hay {orders} pedidos registrados')

df_items['num_order'].value_counts()

# El máximo de líneas de pedidos son 60, mientras que hay pedidos con tan solo una línea. 

# #### *item_id*

# La variable *item_id* se usa para identificar el producto dentro del pedido, por lo tanto un mismo producto que este en pedidos distintos tendrá distintos *item_id*

items = df_items['item_id'].nunique()
print(f'Identificadores : {items}')
print(f'Registros : {df_items.shape[0]}')

# Como se ve no tenemos identificadores de items como registros del dataset lo que nos lleva a pensar que podría haber registros repetidos

df_items['item_id'].value_counts()

df_items[df_items['item_id'] == '642b9b87df5b13e91ce86962684c2613']

# Nuestras sospechas se confirman, ya que vemos que el mismo pedido tiene las tres mismas líneas. Por tanto, se trata de datos duplicados

# #### *created_at*

# Instante en el que se realizó el pedido

df_items['created_at'].sample(1,random_state=seed)

# El formato de la fecha y tiempo es *YYYY-MM-DD HH:MM:SS*

# #### *product_id*

# Identificador del producto

products = df_items['product_id'].nunique()
print(f'Hay {products} identificadores de productos')

# Productos en más frecuentas en los pedidos
df_items['product_id'].value_counts()

# Los productos más comunes en los pedidos, no son exactamente los mismos que los que más cantidad se piden.

# Productos más vendidos (cantidad)
df_best_products = df_items.groupby("product_id",as_index=False).agg({"num_order":"count", "qty_ordered":"sum"}).sort_values("qty_ordered", ascending=False)
df_best_products.head()

# #### *qty_ordered*

# Cantidad pedida de cierto producto para un cierto pedido

df_q = df_items.groupby('num_order',as_index=False).agg(total_qty=('qty_ordered','sum'),n_items=('item_id','count')).sort_values('total_qty',ascending=False)
df_q.head(10)

df_q.tail(10)

# En el resumen de la descripción del conjunto de datos, ya veíamos que los mínimos y máximos de las cantidades que se piden eran bastante dispares. Con esta tabla vemos como se reparten las cantidades pedidas respecto a los productos dentro del pedido.

df_q['total_qty'].value_counts().head(10)

df_q[df_q['total_qty'] > 10].shape[0]

# Vemos que lo más común es que en los pedidos haya entre *2* y *10* unidades de productos. Aun así hay un número sustancia del pedidos que tienen cantidades superiores a las *10* unidades.

# #### *base_cost*

# Precio de coste de una unidad de producto sin IVA

# Como vimos al principio esta columna tiene valores perdidos. Veamos si sería posible recuperarlos mirando *base_cost* del mismo producto en otros pedidos.

products = df_items[df_items['base_cost'].isna()]['product_id'].drop_duplicates().to_list()
print(f'Nº productos con precios nulos: {len(products)}')
df_pr = df_items[(df_items['product_id'].isin(products)) & (df_items['base_cost'].notna())][['product_id','base_cost']]
print(f"Nº productos que se puede recuperar base_cost: {df_pr['product_id'].nunique()}")
df_pr.groupby('product_id',as_index=False).agg(n_cost=('base_cost','count'),mean_cost=('base_cost','mean')).sort_values('n_cost',ascending=False)

# Parece ser que se puede recuperar bastantes valores, eso sí hay que decidir que valores imputar porque los productos varían de precio en los diferentes pedidos. Lo más extraño a mencionar es que el precio de coste medio de uno de los productos sea directamente un cero.

neg_bcost = df_items[df_items['base_cost'] < 0].shape[0]
print(f'Registros con base_cost negativo: {neg_bcost}')

# #### *price*

# Precio unitario de los productos.

pr_zero = df_items[df_items['price'] == 0].shape[0]
null_benefits = df_items[(df_items['price'] < df_items['base_cost'])][['product_id','base_cost','price','discount_percent']]
print(f'Productos con precio cero: {pr_zero}')
print(f'Líneas pedido sin beneficios: {null_benefits.shape[0]}')

# Existen registros con precios nulos y también bastantes líneas de pedidos en los que nos se generan beneficios. Puede que estas últimas se traten de descuentos aplicados.

null_benefits.corr()

# Donde nos surge la gran duda es en que no hay correspondencia entre los costes, precios y descuentos de los productos. Por tanto, podemos intuir que los precios sobre los que se aplican los descuentos no son estos, sino otros, ya que nosotros tenemos los precios finales.

# #### *discount_percent*

# Porcentaje de descuento aplicado, pero no al precio con el que contamos nosotros.

print(f"Líneas con descuento: {df_items[df_items['discount_percent'] > 0].shape[0]}")
print(f'Lineas total: {df_items.shape[0]}')

# Algo que llama la atención es que todos las líneas de pedidos tienen descuentos

print(f"Cantidad de porc. desc.: {df_items['discount_percent'].nunique()}")
df_items['discount_percent'].value_counts().sort_values(ascending=False).head(10)

df_items['discount_percent'].value_counts().sort_values(ascending=False).tail(10)

# Los porcentajes de descuento más comunes son por debajo de *25%*, mientras que los menos comunes oscilan entre el *30% y 50%*.

# #### *customer_id*

# El identificador del cliente que ha realizado el pedido

clients = df_items['customer_id'].nunique()
print(f'{clients} clientes han realizado pedidos')

df_items['customer_id'].sample(5,random_state=seed)

# Este campo es un *hash* con el fin de anonimizar los datos personales.

# #### *city* y *zipcode*

# Estas variables indican la ciudad en la cuál se han realizado el pedido

print(f"Nº de ciudades: {df_items['city'].nunique()}")
print(f"Nº de ciudades: {df_items['zipcode'].nunique()}")

# Vimos antes que estas dos columnas tienen un número similar de registros nulos, lo que da a entender que puede que coincidan.

df_items[(df_items.city.isna()) & (df_items.zipcode.isna())].shape[0]

# Tenemos 2910 registros donde no tenemos ninguna información de la dirección, ni la ciudad ni  el código postal. A pesar de que no son demasiados registros, es información que no podremos recuperar de ninguna manera, ya que los datos están anonimizados

cities = df_items[(df_items['city'].notna()) & (df_items['city'].str.contains('alba'))]['city']
cities.value_counts()

# Haciendo una búsqueda sencilla de cadenas, vemos que el campo no está normalizado y hay nombres de ciudades escritos de diferentes maneras. 

# Normalizamos los nombres de ciudades
indexes = df_items['city'].notna().index
df_items.loc[indexes, 'city'] = df_items.loc[indexes, 'city'].apply(lambda x: str(x).lower().strip())
print(f"Nº de ciudades: {df_items['city'].nunique()}")

# Cuando normalizamos vemos que el número de ciudades disminuye. Aun así, faltarían muchos más métodos que aplicar para llegar a normalizar el campo.

# **NOTA** Por otro lado, mientras que estabamos limpiando datos, nos dimos cuenta que hay valores númericos para ciudades y códigos postales imputados como nombres.

df_city = df_items[(df_items['city'].notna()) & (df_items['city'].str.isnumeric())]
print(f"Nº ciudad con dígitos: {df_city.shape[0]}")
df_city[['num_order','city','zipcode']].sample(5,random_state=seed)

df_zip = df_items[(df_items['zipcode'].notna()) & (df_items['zipcode'].notna())]
df_zip = df_zip[(df_zip['city'].str.isnumeric()) & (~df_zip['zipcode'].str.isnumeric())]
print(f"Nº códigos postales y ciudad invertidos: {df_zip.shape[0]}")
df_zip[['city','zipcode']].sample(10,random_state=seed)

# ### Duplicados

# En apartados anteriores intuimos la presencia de registros duplicados, veamos a que se debe.

print(f"Regitros duplicados: {df_items.duplicated().sum()}")
df_items[df_items.duplicated()].head(5)

# ### Clientes que más compran

# Clientes que más pedidos hacen

df_clients = df_items.groupby(['customer_id', 'num_order'],as_index=False).agg({'qty_ordered':'sum'}) # num_order se repite, por tanto no se puede agregar por el de primeras
df_clients = df_clients.groupby(['customer_id'],as_index=False).agg({'num_order':'count', 'qty_ordered':'sum'})
df_clients = df_clients.sort_values('num_order',ascending=False)
df_clients.head()

# Clientes que han comprado una mayor cantidad de productos

df_clientes = df_clients.sort_values('qty_ordered',ascending=False)
df_clientes.head()

# ### Ventas totales

# En este apartado veremos las fechas en las cuáles se producen más ventas

# Modificamos la columna para que solo muestre la fecha sin la hora
df_items.loc[:, 'date'] = df_items['created_at'].dt.date
df_items[['created_at','date']].sample(5,random_state=seed)

# Agrupamos por fecha para saber la cantidad de productos y el número de pedidos realizados por cada día

df_temp = df_items.groupby(['date', 'num_order']).agg({'qty_ordered':'sum'}).reset_index() # num_order se repite, por tanto no se puede agregar por el de primeras
df_temp = df_temp.groupby(['date']).agg({'num_order':'count', 'qty_ordered':'sum'}).reset_index()
df_temp.sample(5,random_state=seed)

fig = px.area(df_temp, x="date", y="num_order", title='Pedidos realizados por día',)
fig.show()

# Vemos los picos de pedidos en los meses como Noviembre (BlackFriday) y picos en las rebajas tanto de invierno como de verano

fig = px.area(df_temp, x="date", y="qty_ordered", title='Cantidad de productos pedidos por día')
fig.show()

# La cantidad de productos pedidos por día es muy similar en forma a la anterior gráfica, por lo que tienen cierta correlación. Aun así, nos debemos fijar en la escala del eje Y.

df_temp.corr()

# ### Ventas por localización

# Puesto que hemos sacado las coordenadas de cada localización podemos mostrar visualmente donde se han producido más ventas

df_temp = df_items.groupby(['city', 'num_order'],as_index=False).agg({'qty_ordered':'sum'}) # num_order se repite, por tanto no se puede agregar por el de primeras
df_temp = df_temp.groupby(['city'],as_index=False).agg({'num_order':'count', 'qty_ordered':'sum'})
df_temp.sort_values('num_order',ascending=False).head(10)

# Dado que no hemos limpiado aun los datos, los pedidos por localización no son correctos, tal y como podemos ver en la muestra. Algunas ciudades como Madrid se repiten varias veces porque están escritas de forma distinta. Aquí es donde hay que remarcar la importancia de trabajar los datos.

# ## Conclusiones

# A partir de este análisis hemos deducido:
#
# * Una misma localización puede estar representada de muchas diferentes maneras.
# * Los datos que faltan son pocos. De los campos *base_cost*, *city* y *zipcode* que faltan podemos guiarnos de otras ventas para intentar recuperarlos.
# * Hay productos donde el precio base es negativo.
# * Existen productos donde el precio de venta es inferior al precio base.
# * Todos los pedidos tienen aplicado algún tipo de descuento.
# * Existe un pico de ventas en días especiales, sobre todo black friday.

StarcoderdataJupyterScriptsDedupFiltered