Caso de estudio: Cyclistic Bike-share

Análisis de Viajes de Cyclistic

Resumen del proyecto

Este caso de estudio gira en torno a Cyclistic, una empresa de bicicletas compartidas que ha crecido mucho desde su lanzamiento en 2016 en Chicago. Con una flota de 5,824 bicicletas y 692 estaciones, Cyclistic ofrece planes flexibles que atraen a distintos tipos de usuarios: pases individuales o de un día para usuarios ocasionales, y membresías anuales para miembros de Cyclistic. Esa flexibilidad ha ampliado su base de clientes, pero el análisis financiero muestra que las membresías anuales generan más ingresos. Por eso, la directora de marketing, Lily Moreno, quiere diseñar estrategias específicas para convertir a los usuarios ocasionales en miembros anuales. Para lograrlo, es clave analizar las diferencias de comportamiento entre ambos grupos y explorar cómo los medios digitales pueden influir en esa conversión. Este proyecto se basa en el análisis de datos históricos de viajes para identificar patrones y guiar la toma de decisiones de marketing.


Este proyecto capstone forma parte del programa de certificación de Google Data Analytics.
Verificar Certificación


Para ver el dashboard en Tableau, visita el siguiente enlace .

Características principales

  • Rendimiento de ventas por categoría de producto
  • Comparaciones de ventas por región
  • Análisis de tendencias estacionales
  • Patrones de compra de los clientes

Definiendo el problema

El reto principal para Lily Moreno, directora de marketing de Cyclistic, y su equipo de análisis, es diseñar una estrategia efectiva que motive a los usuarios ocasionales a convertirse en miembros anuales. Esta conversión es clave para el crecimiento sostenido de la empresa, ya que los miembros anuales representan una fuente de ingresos más estable y rentable. Para construir una estrategia de marketing bien fundamentada, es necesario entender a fondo el comportamiento de ambos tipos de usuarios y las motivaciones que podrían impulsar un cambio de categoría. En ese sentido, el proyecto se guía por las siguientes preguntas clave:

  • ¿Cómo difiere el uso de las bicicletas de Cyclistic entre los miembros anuales y los usuarios ocasionales?
  • ¿Qué factores podrían motivar a los usuarios ocasionales a comprar una membresía anual?
  • ¿De qué formas puede Cyclistic usar los medios digitales para influir en la decisión de los usuarios ocasionales y fomentar su conversión a miembros?

Objetivo del negocio

A través del análisis histórico, identificar patrones de uso entre miembros anuales y usuarios ocasionales para diseñar estrategias de marketing digital que conviertan a los usuarios ocasionales en miembros anuales.

Tecnologías utilizadas

Estas fueron las tecnologías usadas para desarrollar el proyecto:

  • Python: usado para la limpieza, transformación y análisis de datos. Se utilizó la librería pandas para facilitar el procesamiento y la exploración de los datos.
  • PostgreSQL: usado para almacenar los datos estructurados y ejecutar consultas SQL eficientes que permitieron analizar grandes volúmenes de información.
  • Tableau: herramienta usada para la visualización de datos, permitiendo generar gráficos y dashboards interactivos que facilitan la interpretación de los patrones encontrados.

Base de datos

Para este análisis se usan los datos históricos de viajes en bicicleta de Cyclistic correspondientes a los últimos 12 meses del año 2024. Estos datos fueron proporcionados por Motivate International Inc. y están disponibles en este enlace bajo esta licencia. Por privacidad, se eliminó toda información de usuarios que permitiera identificarlos. Los datos se almacenan en archivos comprimidos en formato .CSV.


-- Monthly Divvy trip data files (2024)
01) 2024-02_divvy_trip-data.csv  
02) 2024-03_divvy_trip-data.csv  
03) 2024-04_divvy_trip-data.csv  
04) 2024-05_divvy_trip-data.csv  
05) 2024-06_divvy_trip-data.csv  
06) 2024-07_divvy_trip-data.csv  
07) 2024-08_divvy_trip-data.csv  
08) 2024-09_divvy_trip-data.csv  
09) 2024-10_divvy_trip-data.csv  
10) 2024-11_divvy_trip-data.csv  
11) 2024-12_divvy_trip-data.csv  
12) 2024-01_divvy_trip-data.csv

Almacenamiento de datos

Se creó una base de datos en PostgreSQL para almacenar los datos. Después de descargar y descomprimir los archivos .zip, los datos se cargaron en el esquema MONTHLY_TRIPS, creado previamente en la base de datos. En total se generaron 12 tablas, una por cada archivo correspondiente a los meses del año, con la siguiente configuración:


CREATE TABLE MONTHLY_TRIPS.tripdata_202401( 
    ride_id VARCHAR(50) PRIMARY KEY,
    rideable_type VARCHAR(50),
    started_at TIMESTAMP,
    ended_at TIMESTAMP,
    start_station_name VARCHAR(100),
    start_station_id VARCHAR(50),
    end_station_name VARCHAR(100),
    end_station_id VARCHAR(50),
    start_lat NUMERIC,
    start_lng NUMERIC,
    end_lat NUMERIC,
    end_lng NUMERIC,
    member_casual VARCHAR(10)
);

Entorno virtual de Python

Una vez almacenadas las tablas en la base de datos, se configuró un entorno virtual en Python para llevar a cabo el procesamiento de los datos. La estructura del proyecto se organizó así:


venv/ # Python virtual environment
config.py # Configuration file with database connection variables
database.py # Functions to connect to and query the database
requirements.txt # List of project dependencies
test_connection.py # Script to verify the database connection
trip_analysis.ipynb # Exploratory trip analysis notebook

  • config.py: archivo de configuración con las variables de conexión a la base de datos.
  • database.py: script que define una clase Database para gestionar la conexión a la base de datos PostgreSQL. Usa la librería psycopg2 y obtiene los parámetros de conexión del archivo config.py. La clase incluye métodos para:
    • Conectarse a la base de datos (connect).
    • Cerrar la conexión de forma segura (disconnect)
    • Verificar si la conexión está activa (is_connected)
    • Ejecutar consultas SQL y devolver los resultados (fetch_all).
    Es una estructura reutilizable que facilita la interacción con la base de datos desde otros módulos del proyecto, promoviendo una separación clara entre la lógica de conexión y el análisis de datos.
  • test_connection.py: script para probar la conexión a la base de datos y verificar que todo funcione correctamente. Es útil para comprobar la conectividad y asegurarse de que los parámetros de configuración en config.py sean correctos antes de continuar con el análisis de datos.
  • data_manager.py: este script define la clase `DataAnalyzer`, diseñada para facilitar el análisis y la limpieza de los datos extraídos de la base de datos. Usa pandas para manipular los datos en formato DataFrame y ofrece funcionalidades clave para preparar la información antes del análisis. Sus métodos principales son:
    • fetch_data(query): ejecuta una consulta SQL a través de una instancia de conexión a la base de datos y devuelve los resultados en un DataFrame de pandas.
    • analyze_null_values(df): muestra estadísticas detalladas de valores nulos por columna y a nivel global en un DataFrame.
    • clean_data(df): realiza tareas de limpieza como reemplazar valores nulos en las columnas de estaciones por 'Unknown' y eliminar filas sin coordenadas de destino.
    • add_time_columns(df): agrega nuevas columnas relacionadas con el tiempo (hora, día de la semana, mes, año y duración del viaje en minutos), basadas en la columna started_at.
    Este módulo permite mantener el procesamiento de datos bien organizado y reutilizable para distintas consultas dentro del análisis exploratorio.
  • trip_analysis.ipynb: este notebook de Jupyter es el núcleo del análisis exploratorio del caso de estudio. Integra consultas a la base de datos PostgreSQL con el procesamiento de datos en pandas.

Procesamiento de datos

Usando el notebook trip_analysis.ipynb, se estableció la conexión a las tablas almacenadas en postgres y se examinó el contenido de cada una de ellas.


# Define the SQL query to fetch data for January 2024
query = "select * from MONTHLY_TRIPS.tripdata_202401;"

# get the data for January 2024
df_202401 = analyzer.fetch_data(query)

Esta tabla contiene la siguiente información:

  • El dataset contiene 144,873 registros y 13 columnas, correspondientes a viajes en bicicleta realizados por usuarios del sistema Cyclistic.
  • Las variables capturan información sobre el tipo de bicicleta, horas de inicio y fin del viaje, estaciones de origen y destino, coordenadas geográficas y tipo de usuario.
  • Las columnas ride_id, rideable_type, started_at, ended_at y member_casual están completas (sin valores nulos).
  • Las estaciones de inicio y fin (nombres e IDs) tienen valores nulos en cerca del 13% al 15% de los registros.
  • Las coordenadas de destino (end_lat, end_lng) tienen una pequeña proporción de valores nulos.
  • Los datos temporales están correctamente formateados como datetime64[ns], mientras que el resto son de tipo object.
Al analizar la información de datos nulos por columna, se encontró lo siguiente:

Null counts per column (descending order):
end_station_name      20749
end_station_id        20749
start_station_id      19165
start_station_name    19165
end_lat                 288
end_lng                 288
started_at                0
rideable_type             0
ride_id                   0
ended_at                  0
start_lat                 0
start_lng                 0
member_casual             0
dtype: int64

Null percentage per column (descending order):
end_station_name      14.32
end_station_id        14.32
start_station_id      13.23
start_station_name    13.23
end_lat                0.20
end_lng                0.20
started_at             0.00
rideable_type          0.00
ride_id                0.00
ended_at               0.00
start_lat              0.00
start_lng              0.00
member_casual          0.00
dtype: float64

Global null value analysis:
- Total cells: 1,883,349
- Total null values: 80,404
- Global null percentage: 4.27%
Visualmente, esta es la distribución de datos nulos por columna:


Distribución de valores nulos

Distribución de valores nulos

Limpieza de datos

Dado que la proporción de datos faltantes supera el 10%, descartar esos registros habría implicado una pérdida de información importante. Por eso se decidió conservar las entradas y manejar los valores nulos asignándolos a una categoría específica: "Unknown".


def clean_data(self, df: pd.DataFrame) -> pd.DataFrame:
    """
    Clean the DataFrame:
    - Fill null values in station columns with 'Unknown'
    - Remove rows without destination coordinates
    
    Args:
    df (pd.DataFrame): DataFrame
    
    Returns:
    pd.DataFrame: cleaned DataFrame
    """
  stations_column = [
      "start_station_name", "start_station_id",
    "end_station_name", "end_station_id"
    ]
  for col in stations_column :
    df[col] = df[col].fillna("Unknown")
    
    df = df.dropna(subset=["end_lat", "end_lng"])
    return df

Como todos los archivos mostraban patrones de datos faltantes similares, se aplicó el mismo tratamiento de forma consistente en todo el dataset.


for month in range(1, 13):
    variable_name = f"df_2024{mes:02}"
    df = globals().get(variable_name )

    if df is not None:
        globals()[variable_name] = analyzer.clean_data(df)

Para apoyar el análisis de datos y descubrir patrones clave para el desarrollo de la estrategia de marketing, se realizaron los siguientes pasos: extracción del día, mes y año a partir de started_at, y cálculo de la duración del viaje.


# Functio to add new columns to the DataFrame
def add_time_columns(self, df: pd.DataFrame) -> pd.DataFrame:
    """        
        Add new columns to the DataFrame based on the 'started_at' column:
            - 'hour': Hour of the trip  
            - 'day_of_week': Day of the week of the trip
            - 'month': Month of the trip
            - 'year': Year of the trip
        Args:
            df (pd.DataFrame): DataFrame with a 'started_at' column of datetime type.
        Returns:
            pd.DataFrame: DataFrame with new columns added.
    """
  # get the trop duration in minutes
  df['trip_duration'] = (df['ended_at'] - df['started_at']).dt.total_seconds() / 60  # Trip duration in minutes
  # Get the day of the week for each trip
  df['day_of_week'] = df['started_at'].dt.day_name()
  # Get the month name for each trip
  df['month'] = df['started_at'].dt.month
  # get the year for each trip
  df['year'] = df['started_at'].dt.year
  
  return df

Esta función se aplicó de forma iterativa a las doce tablas con la siguiente implementación:


for i in range(1, 13):
    df_name = f"df_2024{i:02}"
    df = globals().get(df_name)
    
    if df is not None:
        globals()[df_name] = analyzer.add_time_columns(df)

Luego se extrajeron las variables relevantes y todos los datasets se guardaron como archivos .csv para su análisis posterior


# Define the columns to keep in the final DataFrame
columns_to_keep = [
    'ride_id', 
    'rideable_type', 
    'start_station_name', 
    'start_station_id', 
    'end_station_name', 
    'end_station_id', 
    'start_lat', 
    'start_lng', 
    'end_lat', 
    'end_lng', 
    'member_casual',
    'day_of_week',
    'month', 
    'year',
    'trip_duration'
]

# Store DataFrame in CSV files for each month
for mes in range(1, 13):
    df_name = f'df_2024{mes:02}'
    df = globals()[df_name]  #get the DataFrame from global environment
    df[columns_to_keep].to_csv(f'df_2024{mes:02}.csv', index=False)

Los archivos .csv procesados reemplazaron a los datasets originales y se reorganizaron en cuatro tablas trimestrales (Q1: ene-mar, Q2: abr-jun, Q3: jul-sep, Q4: oct-dic).


CREATE TABLE tripdata2024.q1_tripdata AS
SELECT 
    ride_id, 
    rideable_type, 
    start_station_name, 
    start_station_id, 
    end_station_name, 
    end_station_id, 
    start_lat, 
    start_lng, 
    end_lat, 
    end_lng, 
    member_casual,
    day_of_week,
    month, 
    year,
    trip_duration AS ride_length,
    'Q1' AS quarter
FROM tripdata2024.data202401
UNION DISTINCT
SELECT 
    ride_id, 
    rideable_type, 
    start_station_name, 
    start_station_id, 
    end_station_name, 
    end_station_id, 
    start_lat, 
    start_lng, 
    end_lat, 
    end_lng, 
    member_casual,
    day_of_week,
    month, 
    year,
    trip_duration AS ride_length,
    'Q1' AS quarter
FROM tripdata2024.data202402
UNION DISTINCT
SELECT 
    ride_id, 
    trip_duration AS ride_length,
    rideable_type, 
    start_station_name, 
    start_station_id, 
    end_station_name, 
    end_station_id, 
    start_lat, 
    start_lng, 
    end_lat, 
    end_lng, 
    member_casual,
    day_of_week,
    month, 
    year,
    'Q1' AS quarter
FROM tripdata2024.data202403;

Análisis de datos: análisis exploratorio

Total de usuarios

Tendencia de ventas

La empresa reportó un total de 5.9 millones de usuarios, de los cuales 3.7 millones son miembros actuales y 2.1 millones son usuarios ocasionales.

Tendencia de ventas

Es decir, 63.34% son miembros anuales y 36.66% son usuarios ocasionales.


Total de viajes por trimestre


Se registró el número total de viajes por trimestre. Se encontró que la cantidad de viajes aumenta considerablemente en los trimestres de primavera y verano (Q2 y Q3) y disminuye en los trimestres de invierno (Q1 y Q4), una tendencia típica de los servicios de micromovilidad por factores estacionales. El tercer trimestre (julio-septiembre) fue el de mayor actividad, alcanzando un pico de:

  • 1,339,870 viajes de miembros
  • 983,136 viajes de usuarios ocasionales
  • En cambio, el primer trimestre (enero-marzo) registró la menor cantidad de viajes:
  • 515,138 de miembros
  • 153,584 de usuarios ocasionales
  • Resumen de la distribución por tipo de usuario
En todos los trimestres, los miembros completaron más viajes que los usuarios ocasionales, lo que sugiere un uso más constante y regular por parte de los suscriptores. Sin embargo, la brecha se reduce en Q2 y Q3, lo que podría indicar un aumento de usuarios ocasionales en periodos de buen clima o vacaciones.


Total de viajes por trimestre

Total de viajes por tipo de vehículo

Se registró el total de viajes por tipo de vehículo. El 50.9% de los usuarios tiende a usar bicicletas eléctricas, mientras que el 46.6% usó bicicletas clásicas, y el porcentaje restante eligió patinetas eléctricas.


Total de viajes por tipo de vehículo

Total de viajes por trimestre en 2024, segmentado por tipo de usuario y vehículo


Se analizó el total de viajes realizados en cada trimestre de 2024, segmentado por tipo de usuario (miembro y ocasional) y tipo de vehículo. Se encontró que el primer trimestre (Q1) mostró poca actividad. Los miembros prefirieron tanto bicicletas clásicas como eléctricas, con mayor proporción en las clásicas. En cambio, los usuarios ocasionales tuvieron menor participación en general, pero con un uso equilibrado entre bicicletas clásicas y eléctricas. Durante el segundo trimestre (Q2) se registró un aumento notable en el uso de todos los tipos de vehículo. Las bicicletas eléctricas tuvieron un repunte importante, liderando tanto en volumen de viajes como en preferencia de los usuarios. El tercer trimestre (Q3) marcó el uso más alto en todos los segmentos, con las bicicletas eléctricas como las más usadas. Además, se registraron patinetas eléctricas en ambos tipos de usuario, con mayor uso entre los ocasionales. En el cuarto y último trimestre (Q4) hubo una caída significativa en el volumen de viajes. Aun así, las bicicletas eléctricas se mantuvieron como el vehículo más usado tanto por miembros como por usuarios ocasionales.


Total de viajes por tipo de vehículo y trimestre

Total de viajes por mes y tipo de usuario

El análisis del total de viajes por tipo de usuario a nivel mensual revela lo siguiente:

  • En general, los miembros anuales tienden a usar el servicio de micromovilidad con más frecuencia que los usuarios ocasionales.
  • Durante los primeros cuatro meses (enero-abril), los niveles de actividad son relativamente bajos.
  • De mayo a octubre hay un aumento significativo en el uso de ambos tipos de usuario, siendo septiembre el mes con mayor actividad.
  • Finalmente, en los últimos dos meses (noviembre-diciembre), el volumen de viajes vuelve a bajar, aunque el servicio sigue siendo usado con más frecuencia por los miembros anuales.


Total de usuarios por mes

Total de viajes por día – usuarios miembros y casuales

Se registró el total de viajes diarios, segmentado por tipo de usuario. Los domingos muestran una distribución equilibrada entre ambos grupos, con una ligera mayoría de viajes de miembros anuales (417,067) frente a usuarios ocasionales (368,682). Los lunes, martes y miércoles hay más actividad entre los miembros. En cambio, los jueves, viernes y sábados, la actividad de los miembros tiende a bajar mientras suben los viajes de usuarios ocasionales, reflejando un cambio en los patrones de uso según el día de la semana.


Total de viajes por día - ocasionales y miembros

Recomendación estratégica


Usuarios miembros


Tendencia de ventas

Para los usuarios miembros, el miércoles destaca como el día con mayor volumen de viajes, sobre todo en bicicletas eléctricas (309,425) y clásicas (290,712). Aunque las patinetas eléctricas tienen un uso mucho menor en general, alcanzan un pico modesto el miércoles (9,923). Las bicicletas eléctricas superan de forma constante a las clásicas durante toda la semana. Los niveles más bajos de actividad entre los miembros ocurren los sábados y domingos, lo que indica un patrón de uso ligado principalmente a los traslados entre semana.

Usuarios ocasionales


Distribución por categoría

Para los usuarios ocasionales, el sábado registra el mayor número de viajes, con un uso equilibrado entre bicicletas eléctricas (271,032) y clásicas (213,594). Aunque el uso de patinetas eléctricas es mucho menor, mantiene una presencia constante durante toda la semana. Las bicicletas eléctricas superan ligeramente a las clásicas todos los días. La menor actividad se observa los lunes, martes y miércoles, lo que sugiere que los usuarios ocasionales usan el servicio principalmente con fines recreativos los fines de semana y días no laborables.

Recomendación estratégica


A partir del análisis realizado, se identificaron los siguientes hallazgos clave:

  • Los usuarios ocasionales tienden a usar el servicio con más frecuencia los fines de semana, especialmente los sábados, principalmente con fines recreativos.
  • Muestran una ligera preferencia por las bicicletas eléctricas sobre las clásicas.
  • Su actividad entre semana es baja, a diferencia de los miembros anuales, que usan el servicio sobre todo de lunes a miércoles, probablemente para traslados o movilidad rutinaria.


Estrategia recomendada

Diseñar campañas dirigidas que resalten el valor de un uso frecuente, orientadas a que los usuarios ocasionales se conviertan en miembros anuales. Acciones sugeridas:

  • Promociones específicas de fin de semana, como:
    • Membresías de prueba gratis válidas por un fin de semana.
    • Descuentos para usuarios que completen más de "X" viajes en un mes.
  • Campañas educativas por redes sociales y la app para comunicar:
    • El ahorro de una membresía frente a los pases de viaje individual.
    • Beneficios exclusivos como acceso anticipado a eventos o estacionamiento preferente en estaciones de alta demanda
  • Mensajes personalizados dentro de la app:
    • Cuando se detecten patrones de uso recreativo, sugerir que una membresía anual permite usar el servicio entre semana sin costo adicional.
    • Recomendar destinos o rutas "populares" que suelen usar los miembros.
  • Reforzar el valor percibido:
    • Compartir testimonios de usuarios que empezaron como ocasionales y se convirtieron en miembros satisfechos.
    • Asociar la membresía con un estilo de vida activo y un sentido de comunidad.
  • Alianzas estratégicas:
    • Colaborar con gimnasios, cafeterías o centros culturales para ofrecer beneficios adicionales a los miembros.