{"id":22438969,"url":"https://github.com/dcostachar/cyclistic-case-study","last_synced_at":"2025-03-27T09:23:19.331Z","repository":{"id":266561352,"uuid":"888621673","full_name":"dcostachar/cyclistic-case-study","owner":"dcostachar","description":"An analysis of Cyclistic bike-share data with SQL and Tableau to uncover usage trends and generate marketing strategies to boost annual memberships.","archived":false,"fork":false,"pushed_at":"2024-12-04T21:43:37.000Z","size":5503,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-01T14:18:21.425Z","etag":null,"topics":["consumer-behaviour-analysis","data-visualization","exploratory-data-analysis","marketing-analytics","mysql","sql","tableau"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dcostachar.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-11-14T18:01:26.000Z","updated_at":"2024-12-04T21:43:40.000Z","dependencies_parsed_at":"2024-12-04T22:42:40.595Z","dependency_job_id":null,"html_url":"https://github.com/dcostachar/cyclistic-case-study","commit_stats":null,"previous_names":["dcostachar/cyclistic-case-study"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dcostachar%2Fcyclistic-case-study","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dcostachar%2Fcyclistic-case-study/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dcostachar%2Fcyclistic-case-study/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dcostachar%2Fcyclistic-case-study/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dcostachar","download_url":"https://codeload.github.com/dcostachar/cyclistic-case-study/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245815136,"owners_count":20676872,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["consumer-behaviour-analysis","data-visualization","exploratory-data-analysis","marketing-analytics","mysql","sql","tableau"],"created_at":"2024-12-06T01:12:17.016Z","updated_at":"2025-03-27T09:23:19.291Z","avatar_url":"https://github.com/dcostachar.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# Cyclistic Case Study: How Does A Bike-Share Company Navigate Speedy Success? \n\nAuthor: Charlene D'Costa \u003cbr /\u003e\nDate: November 1, 2024 \u003cbr /\u003e\nCapstone project for the Google Data Analytics Professional Certificate. \u003cbr /\u003e\n\n[Tableau Dashboard](https://public.tableau.com/app/profile/charlene.d.costa/viz/CyclisticBikeShareAnalysisDashboard_17280817981870/CyclisticBikeShareAnalysisDashboard) \u003cbr /\u003e\n[Divvy Bike Share Datasets](https://github.com/dcostachar/cyclistic-case-study/tree/main/datasets)\n\n# Phase 1: Ask \n\n\u003cdetails\u003e\n  \u003csummary\u003eDefining the business problem.\u003c/summary\u003e\n\n## 1.1 Project Overview\n\nCyclistic is a bike-share company based in Chicago, offering a diverse range of over 5,800 bicycles and 600 docking stations throughout the city. The company sets itself apart by providing inclusive options like reclining bikes, hand tricycles, and cargo bikes, catering to people with disabilities and those who prefer alternative bike types. While the majority of users ride for leisure, 30% utilize Cyclistic bikes for their daily commutes.\n\nSince its launch in 2016, Cyclistic has rapidly expanded, becoming a key player in Chicago's urban mobility landscape. The company offers flexible pricing plans, including single-ride passes, full-day passes, and annual memberships. Cyclistic’s finance team has identified that annual members are significantly more profitable than casual riders, prompting the director of marketing, Lily Moreno, to focus on converting casual riders into annual members. Moreno believes that a deeper understanding of the usage patterns between casual riders and annual members is essential to achieve this.\n\n## 1.2 Business Task\n\nAnalyze Cyclistic historical bike trip data to understand the differences in usage patterns between casual riders and annual members. Use these insights to inform the development of a targeted marketing strategy to convert casual riders into annual members, ultimately driving Cyclistic’s growth and profitability.\n\n## 1.3 Key Stakeholders\n\n* **Lily Moreno:** Director of Marketing at Cyclistic, responsible for overseeing the marketing strategy and driving the initiative to increase annual memberships.\n* **Cyclistic Marketing Analytics Team:** A group of data analysts responsible for collecting, analyzing, and reporting data that helps guide Cyclistic's marketing strategies.\n* **Cyclistic Executive Team:** The decision-making body that will evaluate and approve the proposed marketing strategy based on the analysis and recommendations.\n\n\u003c/details\u003e\n\n# Phase 2: Prepare\n\n\u003cdetails\u003e\n  \u003csummary\u003eCollecting and validating relevant data for analysis.\u003c/summary\u003e\n\n## 2.1 About the Dataset\n\nSince Cyclistic is a fictional company, the Google Data Analytics program has recommended using data from Chicago's Divvy bicycle-sharing service for this case study. This data is provided by Motivate International Inc. under a specific data license agreement. The dataset used in this case study spans 12 months of trip data from 2023, covering over 5,800 bicycles across 600 docking stations. It includes user usage data, including bike types, start and end times, start and end stations, ride duration, and user types (casual or member).\n\n## 2.2 Data Compliance and Accessibility\n\nThe data is publicly available from Lyft Bikes and Scooters, LLC under a non-exclusive, royalty-free, and perpetual license. Users can access, reproduce, analyze, and distribute the data for any lawful purpose, with certain conditions. The dataset cannot be used unlawfully, sold as a stand-alone commercial product, or linked to personally identifiable customer information.\n\n## 2.3 Data Integrity and Credibility\n\nThe dataset is sourced from a reliable and publicly accessible platform, ensuring its credibility for analytical purposes. While it is comprehensive in terms of ride details, it lacks personal demographic information about the riders, limiting the ability to conduct analyses that link ride data to specific user demographics. However, the data's reliability is bolstered by its currentness (from 2023), consistent format, and comprehensive coverage over an entire year, providing a robust foundation for analysis.\n\n## 2.4 Data Organization and Verification\n\nThe dataset is organized into 12 CSV files, each representing one month of the year and containing detailed ride information. The data is presented in a long format, where each row corresponds to a single observation linked to a unique ride ID, and each column captures a specific attribute of that ride, including bike type, start and end times, start and end stations, ride duration, and user type. This structured organization allows for efficient data processing, cleaning, and analysis. \n\n\u003c/details\u003e\n\n# Phase 3: Process\n\n\u003cdetails\u003e\n  \u003csummary\u003eCleaning and transforming data for analysis. \u003c/summary\u003e\n\n## 3.1 Importing the Data\n\n### Creating a SQL Table and Importing CSV Data\n\nIn this phase, I will clean and transform the data to prepare it for analysis. I will use Docker to run\na MySQL database and connect it to DataGrip, my integrated development environment (IDE), to perform the analysis.\n\nFirst, I will download 12 months of trip data from 2023, with each month’s data stored in a separate CSV file. Upon\ninspecting the data, I observe that all 12 CSV files follow a consistent format in terms of the number of columns,\ncolumn names, and data types. I also identify that `ride_id` serves as a primary key (i.e., the unique identifier for\neach record). Now that the structure of the data is understood, I can create my table in SQL to store the data. This\ninvolves mapping the columns from the CSV files to the SQL table and assigning the appropriate data types.\n\n```sql \nCREATE TABLE trips\n(\n    ride_id            VARCHAR(32),\n    rideable_type      VARCHAR(32),\n    started_at         DATETIME,\n    ended_at           DATETIME,\n    start_station_name VARCHAR(100),\n    start_station_id   VARCHAR(100),\n    end_station_name   VARCHAR(100),\n    end_station_id     VARCHAR(20),\n    start_lat          DECIMAL(10, 8),\n    start_lng          DECIMAL(11, 8),\n    end_lat            DECIMAL(10, 8),\n    end_lng            DECIMAL(11, 8),\n    member_casual      VARCHAR(32)\n);\n```\n\n## 3.2 Data Validation\n\nChecking the number of characters in each `ride_id`. Each `ride_id` has the same number of characters (16).\n\n```sql\nSELECT LENGTH(ride_id) AS ride_id_length, COUNT(*) AS ride_id_length_count\nFROM trips\nGROUP BY ride_id_length;\n```\n\nChecking the dataset contains only data from 2023. There are 45 rows containing trip data from 2024. However, upon\nfurther inspection, each of these records corresponds to a trip that started on December 31, 2023, and concluded the\nnext day on January 1, 2024. This confirms that there are no inaccuracies with the data in these records.\n\n```sql\nSELECT *\nFROM trips\nWHERE YEAR (started_at) != 2023\n   OR YEAR (ended_at) != 2023;\n```\n\nChecking the dataset contains all 12 months from 2023. Confirmed that they do.\n\n```sql\nSELECT DISTINCT MONTH (started_at) AS month\nFROM trips\nORDER BY month;\n\nSELECT DISTINCT MONTH (ended_at) AS month\nFROM trips\nORDER BY month;\n```\n\nChecking the number of bike types. There are 3: electric, classic, and docked.\n\n```sql\nSELECT DISTINCT(rideable_type), COUNT(rideable_type)\nFROM trips\nGROUP BY rideable_type\nORDER BY COUNT(rideable_type) DESC; \n```\n\nChecking the number of membership types. There are 2: member and casual.\n\n```sql \nSELECT DISTINCT (member_casual), COUNT(member_casual)\nfrom trips\nGROUP BY member_casual\nORDER BY COUNT(member_casual) DESC;\n```\n\n## 3.3 Data Cleaning\n\n### Identifying and Removing Duplicates\n\nNo duplicated `ride_id`. This is important as `ride_id` serves as a primary key.\n\n```sql\nSELECT COUNT(ride_id) - COUNT(distinct ride_id) AS duplicate_rows\nFROM trips;\n```\n\n### Identifying and Handling NULL Values\n\nChecking for the number of NULL values in each column of the table. I observed that the station-related data, along\nwith the corresponding latitude and longitude data, contain the majority of the NULL values. Therefore, I will focus\nmost of my time on cleaning this part of the dataset. Here is a breakdown of columns with NULL values:\n\n- `start_station_name`: 875716\n- `start_station_id`: 875848\n- `end_station_name`: 929202\n- `end_station_id`: 929343\n- `end_lat`: 6990\n- `end_long`: 6990\n\n```sql\nSELECT COUNT(*) - COUNT(ride_id)            AS ride_id,\n       COUNT(*) - COUNT(rideable_type)      AS rideable_type,\n       COUNT(*) - COUNT(started_at)         AS started_at,\n       COUNT(*) - COUNT(ended_at)           AS ended_at,\n       COUNT(*) - COUNT(start_station_name) AS start_station_name,\n       COUNT(*) - COUNT(start_station_id)   AS start_station_id,\n       COUNT(*) - COUNT(end_station_name)   AS end_station_name,\n       COUNT(*) - COUNT(end_station_id)     AS end_station_id,\n       COUNT(*) - COUNT(start_lat)          AS start_lat,\n       COUNT(*) - COUNT(start_lng)          AS start_lng,\n       COUNT(*) - COUNT(end_lat)            AS end_lat,\n       COUNT(*) - COUNT(end_lng)            AS end_lng,\n       COUNT(*) - COUNT(member_casual)      AS member_casual\nFROM trips;\n```\n\n### Creating and Cleaning the Station Data Table\n\nI'll begin by creating a new table called `station_data_cleaned` containing the station-related data. Creating a new\ntable from the existing one will give me a safe and structured environment to clean and validate my data. It preserves\nthe original data set and allows for focused data manipulation without the risk of corrupting the source data. After\ncleaning the data in my `station_data_cleaned`, I will merge it back into the original table in a controlled manner,\nensuring accuracy and integrity in the final dataset.\n\nBecause I want to clean all station names, I will combine both the start and end station names into a single column\ncalled `station_name`, which corresponds to the appropriate `station_id`.\n\n```sql\nCREATE TABLE station_data_cleaned\n(\n    station_name VARCHAR(100),\n    station_id   VARCHAR(100)\n);\n\nINSERT INTO station_data_cleaned (station_name, station_id)\nSELECT trips.start_station_name, trips.start_station_id\nFROM trips;\n\nINSERT INTO station_data_cleaned (station_name, station_id)\nSELECT trips.end_station_name, trips.end_station_id\nFROM trips;\n```\n\nNow that I've created a separate table, I'll proceed with cleaning the station data by applying various functions to\nclean the string values.\n\nI'll start by removing the \"Public Rack\" prefix in station names to standardize the station names for consistency and\ncomparison.\n\n```sql\nUPDATE station_data_cleaned\nSET station_name = TRIM(REPLACE(station_name, 'Public Rack - ', ''))\nWHERE station_name LIKE 'Public Rack%';\n```\n\nI will apply the same logic for removing the suffix \"(Temp)\".\n\n```sql\nUPDATE station_data_cleaned\nSET station_name = TRIM(REPLACE(station_name, '(Temp)', ''))\nWHERE station_name LIKE '% (Temp)';\n```\n\nNow I will remove any leading or trailing spaces from `station_names`.\n\n```sql\nUPDATE station_data_cleaned\nSET station_name = TRIM(station_name);\n```\n\nNext, I'll convert all station names to lowercase to eliminate any inconsistent casing.\n\n```sql\nUPDATE station_data_cleaned\nSET station_name = LOWER(station_name);\n```\n\n### Identifying and Handling Missing Data in Station Data\n\nNow that I've cleaned my data, I want to investigate instances where a station name exists without a corresponding\nstation ID, and vice versa. This is important because I need to join on the station ID to connect the cleaned data to\nmy source table. Therefore, I want to identify how many stations are missing IDs.\n\nFrom the queries below, I can see that there are no instances where the station name is NULL and the station ID is not.\nHowever, there are 273 rows where a station name exists but no station ID is present. Upon further inspection, I found\nthat two stations appear repeatedly in the list of 273 records: Elizabeth St \u0026 Randolph St and Stony Island Ave \u0026 63rd\nSt.\n\n```sql\nSELECT *\nFROM station_data_cleaned\nWHERE station_name IS NOT NULL\n  AND station_id IS NULL;\n\nSELECT *\nFROM station_data_cleaned\nWHERE station_name IS NULL\n  AND station_id IS NOT NULL;\n\nSELECT COUNT(DISTINCT station_name)\nFROM station_data_cleaned\nWHERE station_name IS NOT NULL\n  AND station_id IS NULL;\n```\n\nI will now perform a fuzzy match on these station names to compare them with the other names in the station data table.\nThis will help us determine if they might match another station name in the table and already have an ID, with the\nmissing ID potentially due to a data entry error or a similar issue.\n\nGood news! I found a station ID match for Elizabeth St \u0026 Randolph St. I will now insert the correct ID, 23001, into\nall rows that have a missing ID for this station.\n\n```sql\nSELECT station_name,\n       MIN(station_id)\nFROM station_data_cleaned\nGROUP BY station_name\nHAVING station_name LIKE 'elizabeth%'\n\nUPDATE station_data_cleaned\nSET station_id = '23001'\nWHERE station_name = 'elizabeth st \u0026 randolph st'\n  AND station_id IS NULL;\n```\n\nMore good news! A station ID match was also found for Stony Island Ave \u0026 63rd St. I will now insert the correct ID,\n653B, into the rows where the ID is missing.\n\n```sql\nSELECT station_name,\n       MIN(station_id)\nFROM station_data_cleaned\nGROUP BY station_name\nHAVING station_name LIKE 'stony island%'\n\nUPDATE station_data_cleaned\nSET station_id = '653B'\nWHERE station_name = 'stony island ave \u0026 63rd st'\n  AND station_id IS NULL;\n```\n\n### Identifying and Handling NULL Values in Station Data\n\nI'll run the same query as above to verify that the station IDs were updated correctly and to confirm that there are no\nmore NULL station IDs in our station data table.\n\n```sql\nSELECT *\nFROM station_data_cleaned\nWHERE station_name IS NOT NULL\n  AND station_id IS NULL;\n```\n\nSince there are no issues, I'll remove the rows where the `station_name` and `station_id` are NULL.\n\n```sql\nDELETE\nFROM station_data_cleaned\nWHERE station_name IS NULL\n  AND station_id IS NULL;\n```\n\n### Identifying and Removing Duplicates in Station Data\n\nMy last step is to remove the duplicates from the `station_data_cleaned` table so that each row is unique (i.e. all\nstation names and ids are distinct) and my upcoming joins can be carried out correctly.\n\nIf I simply use SELECT DISTINCT on `station_name` and `station_id`, I may encounter situations where the same station\nname is incorrectly linked to multiple IDs or vice versa. To avoid this, instead of using SELECT DISTINCT, I'll use a\nGROUP BY on both `station_name` and `station_id` to identify which grouping provides fewer rows. This will help me\nprevent the issue described earlier.\n\nGrouping by station_name returns 1582 rows, while grouping by station_id returns 1537 rows. This indicates that there\nare more duplicate station names (i.e., multiple names linked to different IDs). Therefore, I will use the grouping by\nstation id to create my final station lookup table, as it results in fewer rows and eliminates duplicate data when\ngrouped by ID.\n\n```sql\nSELECT COUNT(*)\nFROM (SELECT station_name, MIN(station_id) as station_id\n      FROM station_data_cleaned\n      GROUP BY station_name) AS station_name_grouping;\n\nSELECT COUNT(*)\nFROM (SELECT station_id, MIN(station_name) as station_name\n      FROM station_data_cleaned\n      GROUP BY station_id) AS station_id_grouping;\n```\n\n### Saving the Cleaned Station Data Table\n\nCreating my final station lookup table with the cleaned station data, where each row is unique, with distinct station\nnames and IDs. This will ensure that the upcoming joins are performed correctly. To do this, I'll use a CTAS\ncommand (Create Table As Select), which allows me to create a new table and populate it with the result set of a SELECT\nquery (the one used above), effectively combining both table creation and data insertion in a single step.\n\nUpon creating my final station lookup table with the cleaned station data and giving it a final review, I noticed a few\nstation names and IDs that appear to be duplicates (e.g., the same station name with minor differences such as special\ncharacters) or station names with \"test\" in the name, which seem to be invalid/inaccurate entries. Cleaning this\nthoroughly would require a significant amount of time to review each row in detail. However, I’ve decided to timebox\nthis task, and for the purpose of this assignment, I will proceed knowing that I’ve already done as thorough a job as\npossible cleaning the data while preserving as much of it as I could. Additionally, in some cases changing the station\nID in the final station lookup table could result in missing rows when the join is performed with the trips table. This\nis another reason why I have decided to hold off on cleaning the final station lookup table data further.\n\n```sql\nCREATE TABLE final_station_data_cleaned AS\nSELECT station_id, MIN(station_name) as station_name\nFROM station_data_cleaned\nGROUP BY station_id\n```\n\n### Preparing the Source Table for Merging with the Cleaned Station Data Table\n\nMoving on to the next task: preparing the source table for merging with the cleaned data. This involves standardizing\nthe station names (string data) in the source table to match the formatting of the station names in the\n`final_station_data_cleaned` table, ensuring the upcoming joins are carried out correctly.\n\nI'll repeat the same steps as above, starting with removing the \"Public Rack\" prefix in station names to standardize\nthe station names for consistency and comparison. I'll check my work before updating the rows in the source table for\neach step.\n\n```sql\nSELECT start_station_name                                      AS start_station_name_original,\n       TRIM(REPLACE(start_station_name, 'Public Rack - ', '')) AS start_station_name_after,\n       end_station_name                                        AS end_station_name_original,\n       TRIM(REPLACE(end_station_name, 'Public Rack - ', ''))   AS end_station_name_after\nFROM trips\nWHERE start_station_name LIKE 'Public Rack%'\n   OR end_station_name LIKE 'Public Rack%';\n\nUPDATE trips\nSET start_station_name = TRIM(REPLACE(start_station_name, 'Public Rack - ', ''))\nWHERE start_station_name LIKE 'Public Rack%';\n\nUPDATE trips\nSET end_station_name = TRIM(REPLACE(end_station_name, 'Public Rack - ', ''))\nWHERE end_station_name LIKE 'Public Rack%';\n\n```\n\nI will apply the same logic for removing the suffix \"(Temp)\".\n\n```sql\nSELECT start_station_name                              AS start_station_name_original,\n       TRIM(REPLACE(start_station_name, '(Temp)', '')) AS start_station_name_after,\n       end_station_name                                AS end_station_name_original,\n       TRIM(REPLACE(end_station_name, '(Temp)', ''))   AS end_station_name_after\nFROM trips\nWHERE start_station_name LIKE '% (Temp)'\n   OR end_station_name LIKE '% (Temp)';\n\nUPDATE trips\nSET start_station_name = TRIM(REPLACE(start_station_name, '(Temp)', ''))\nWHERE start_station_name LIKE '% (Temp)';\n\nUPDATE trips\nSET end_station_name = TRIM(REPLACE(end_station_name, '(Temp)', ''))\nWHERE end_station_name LIKE '% (Temp)';\n```\n\nNow I will remove any leading or trailing spaces from `station_names`.\n\n```sql\nSELECT start_station_name       AS start_station_name_original,\n       TRIM(start_station_name) AS start_station_name_after,\n       end_station_name         AS end_station_name_original,\n       TRIM(end_station_name)   AS end_station_name_after\nFROM trips;\n\nUPDATE trips\nSET start_station_name = TRIM(start_station_name);\n\nUPDATE trips\nSET end_station_name = TRIM(end_station_name);\n```\n\nNext, I'll convert all station names to lowercase to eliminate any inconsistent casing.\n\n```sql\nSELECT start_station_name        AS start_station_name_original,\n       LOWER(start_station_name) AS start_station_name_after,\n       end_station_name          AS end_station_name_original,\n       LOWER(end_station_name)   AS end_station_name_after\nFROM trips;\n\nUPDATE trips\nSET start_station_name = LOWER(start_station_name);\n\nUPDATE trips\nSET end_station_name = LOWER(end_station_name);\n```\n\nNow that I've standardized the station names (string data) in the source table, I will remove the rows where the\nstation data is fully or partially incomplete.\n\n```sql\nDELETE\nFROM trips\nWHERE (start_station_name IS NULL AND start_station_id IS NULL)\n   OR (end_station_name IS NULL AND end_station_id IS NULL);\n```\n\n### Joining the Cleaned Source Table with the Cleaned Station Data Table\n\nTo join the source table `trips` with the cleaned station data table `final_station_data_cleaned` I will use Common\nTable Expressions—temporary tables created from select statements using the WITH command—to update the\n`start_station_id` and `end_station_id` in the `trips` table by joining them with the corresponding `station_id` from\nthe `final_station_data_cleaned` table.\n\nIn the first CTE I will fix the `start_station_id` for each trip by performing an INNER JOIN between the `trips` table\nand `final_station_data_cleaned` table, matching the `start_station_id` in `trips` with the `station_id` in the\n`final_station_data_cleaned` table. Building upon the first CTE, the second CTE will take the results of the first CTE\nand fix the `end_station_id` for each trip by performing an INNER JOIN between the output of the first CTE, which is the\ncreation of the `start_station_id_fixed_trips` table and the `final_station_data_cleaned` table. Here, I matched the\n`end_station_id` in `trips` with the `station_id` in the `final_station_data_cleaned` table. This ensures that\nboth the `start_station_id` and `end_station_id` are updated correctly for each trip.\n\nThe final SELECT * retrieves all rows from the `start_and_end_station_id_fixed_trips` CTE, which now includes fixed\nvalues for both `start_station_id` and `end_station_id`.\n\nThis entire query ensures that both the start and end station IDs are correctly mapped to the cleaned station data for\nall trips, providing a cleaner and more accurate dataset. The query ultimately affects 4,331,823 rows, reflecting all\nthe trips where station IDs have been corrected or updated.\n\n```sql\nWITH start_station_id_fixed_trips AS (SELECT trips.ride_id,\n                                             trips.rideable_type,\n                                             trips.started_at,\n                                             trips.ended_at,\n                                             station.station_name as start_station_name,\n                                             station.station_id   as start_station_id,\n                                             trips.end_station_name,\n                                             trips.end_station_id,\n                                             trips.start_lat,\n                                             trips.start_lng,\n                                             trips.end_lat,\n                                             trips.end_lng,\n                                             trips.member_casual\n                                      FROM final_station_data_cleaned station\n                                               INNER JOIN trips ON station.station_id = trips.start_station_id),\n\n     start_and_end_station_id_fixed_trips AS (SELECT start_station_id_fixed_trips.ride_id,\n                                                     start_station_id_fixed_trips.rideable_type,\n                                                     start_station_id_fixed_trips.started_at,\n                                                     start_station_id_fixed_trips.ended_at,\n                                                     start_station_id_fixed_trips.start_station_name,\n                                                     start_station_id_fixed_trips.start_station_id,\n                                                     station.station_name as end_station_name,\n                                                     station.station_id   as end_station_id,\n                                                     start_station_id_fixed_trips.start_lat,\n                                                     start_station_id_fixed_trips.start_lng,\n                                                     start_station_id_fixed_trips.end_lat,\n                                                     start_station_id_fixed_trips.end_lng,\n                                                     start_station_id_fixed_trips.member_casual\n                                              FROM final_station_data_cleaned station\n                                                       INNER JOIN start_station_id_fixed_trips\n                                                                  ON station.station_id = start_station_id_fixed_trips.end_station_id)\nSELECT *\nFROM start_and_end_station_id_fixed_trips;\n-- 4,331,823 rows\n```\n\nCreating my final table, with my cleaned, normalized, and merged data.\n\n```sql\nCREATE TABLE trips_cleaned AS\nWITH start_station_id_fixed_trips AS (SELECT trips.ride_id,\n                                             trips.rideable_type,\n                                             trips.started_at,\n                                             trips.ended_at,\n                                             station.station_name as start_station_name,\n                                             station.station_id   as start_station_id,\n                                             trips.end_station_name,\n                                             trips.end_station_id,\n                                             trips.start_lat,\n                                             trips.start_lng,\n                                             trips.end_lat,\n                                             trips.end_lng,\n                                             trips.member_casual\n                                      FROM final_station_data_cleaned station\n                                               INNER JOIN trips ON station.station_id = trips.start_station_id),\n\n     start_and_end_station_id_fixed_trips AS (SELECT start_station_id_fixed_trips.ride_id,\n                                                     start_station_id_fixed_trips.rideable_type,\n                                                     start_station_id_fixed_trips.started_at,\n                                                     start_station_id_fixed_trips.ended_at,\n                                                     start_station_id_fixed_trips.start_station_name,\n                                                     start_station_id_fixed_trips.start_station_id,\n                                                     station.station_name as end_station_name,\n                                                     station.station_id   as end_station_id,\n                                                     start_station_id_fixed_trips.start_lat,\n                                                     start_station_id_fixed_trips.start_lng,\n                                                     start_station_id_fixed_trips.end_lat,\n                                                     start_station_id_fixed_trips.end_lng,\n                                                     start_station_id_fixed_trips.member_casual\n                                              FROM final_station_data_cleaned station\n                                                       INNER JOIN start_station_id_fixed_trips\n                                                                  ON station.station_id = start_station_id_fixed_trips.end_station_id)\nSELECT *\nFROM start_and_end_station_id_fixed_trips;\n```\n\u003c/details\u003e\n\n# Phase 4: Analyze\n\n\u003cdetails\u003e\n  \u003csummary\u003eAnalyzing data using SQL to uncover trends and generate insights.\u003c/summary\u003e\n\n## 4.1 Bike Usage Patterns\n\n### Casuals vs. Members: Most and Least Used Bike Types\n\nLet's begin by getting a breakdown of casuals and members in the dataset. I see that there are ~2.8 million members\nand ~1.5 million casuals.\n\n```sql\nSELECT member_casual, COUNT(member_casual)\nFROM trips_cleaned\nGROUP BY member_casual;\n```\n\nBike usage patterns for casuals vs. members. Classic bikes are more popular than electric bikes across both\ncategories, while docked bikes are only used by casuals.\n\n```sql\nSELECT rideable_type, COUNT(rideable_type)\nFROM trips_cleaned\nWHERE member_casual = 'casual'\nGROUP BY rideable_type\nORDER BY COUNT(rideable_type) DESC;\n\nSELECT rideable_type, COUNT(rideable_type)\nFROM trips_cleaned\nWHERE member_casual = 'member'\nGROUP BY rideable_type\nORDER BY COUNT(rideable_type) DESC;\n```\n\n## 4.2 Trip Distance and Duration Trends\n\n### Casuals vs. Members: Most and Least Popular Start and End Stations\n\n10 most popular start stations for casuals vs. members.\n\n```sql\nSELECT start_station_name, COUNT(start_station_name)\nFROM trips_cleaned\nWHERE member_casual = 'casual'\nGROUP BY start_station_name\nORDER BY COUNT(start_station_name) DESC LIMIT 10;\n\nSELECT start_station_name, COUNT(start_station_name)\nFROM trips_cleaned\nWHERE member_casual = 'member'\nGROUP BY start_station_name\nORDER BY COUNT(start_station_name) DESC LIMIT 10;\n```\n\nCommon popular start stations. There are none.\n\n```sql\nWITH casual_ss AS (SELECT start_station_name, COUNT(start_station_name)\n                   FROM trips_cleaned\n                   WHERE member_casual = 'casual'\n                   GROUP BY start_station_name\n                   ORDER BY COUNT(start_station_name) DESC\n    LIMIT 10\n    )\n   , member_ss AS (\nSELECT start_station_name, COUNT (start_station_name)\nFROM trips_cleaned\nWHERE member_casual = 'member'\nGROUP BY start_station_name\nORDER BY COUNT (start_station_name) DESC\n    LIMIT 10\n    )\nSELECT casual_ss.start_station_name\nFROM casual_ss\n         INNER JOIN member_ss\n                    ON casual_ss.start_station_name = member_ss.start_station_name;\n```\n\nLeast popular start stations for casuals vs. members determined by \u003c 10 visits in total for the year. 233 stations in\ncommon.\n\n```sql\nSELECT start_station_name, COUNT(start_station_name) AS cnt_start_station_name_casual\nFROM trips_cleaned\nWHERE member_casual = 'casual'\nGROUP BY start_station_name\nHAVING cnt_start_station_name_casual \u003c 10;\n-- 391 stations\n\nSELECT start_station_name, COUNT(start_station_name) AS cnt_start_station_name_member\nFROM trips_cleaned\nWHERE member_casual = 'member'\nGROUP BY start_station_name\nHAVING cnt_start_station_name_member \u003c 10;\n-- 367 stations\n```\n\n10 most popular end stations for casuals vs. members.\n\n```sql\nSELECT end_station_name, COUNT(end_station_name)\nFROM trips_cleaned\nWHERE member_casual = 'casual'\nGROUP BY end_station_name\nORDER BY COUNT(end_station_name) DESC LIMIT 10;\n\nSELECT end_station_name, COUNT(end_station_name)\nFROM trips_cleaned\nWHERE member_casual = 'member'\nGROUP BY end_station_name\nORDER BY COUNT(end_station_name) DESC LIMIT 10;\n```\n\nCommon popular end stations. 1 station in common: wells st \u0026 concord ln.\n\n```sql\nWITH casual_es AS (SELECT end_station_name, COUNT(end_station_name) AS cnt_casual\n                   FROM trips_cleaned\n                   WHERE member_casual = 'casual'\n                   GROUP BY end_station_name\n                   ORDER BY COUNT(end_station_name) DESC\n    LIMIT 10\n    )\n   , member_es AS (\nSELECT end_station_name, COUNT (end_station_name) AS cnt_member\nFROM trips_cleaned\nWHERE member_casual = 'member'\nGROUP BY end_station_name\nORDER BY COUNT (end_station_name) DESC\n    LIMIT 10\n    )\nSELECT casual_es.end_station_name\nFROM casual_es\n         INNER JOIN member_es\n                    ON casual_es.end_station_name = member_es.end_station_name;\n```\n\nLeast popular end stations for casuals vs. members determined by \u003c 10 visits in total for the year. 233 stations in\ncommon.\n\n```sql\nSELECT end_station_name, COUNT(end_station_name) AS cnt_end_station_name_casual\nFROM trips_cleaned\nWHERE member_casual = 'casual'\nGROUP BY end_station_name\nHAVING cnt_end_station_name_casual \u003c 10;\n-- 402 stations\n\nSELECT end_station_name, COUNT(end_station_name) AS cnt_end_station_name_member\nFROM trips_cleaned\nWHERE member_casual = 'member'\nGROUP BY end_station_name\nHAVING cnt_end_station_name_member \u003c 10;\n-- 370 stations\n```\n\n### Casuals vs. Members: Most Popular Trips\n\n10 most popular trips for casuals vs. members determined by `start_station_name` to `end_station_name`.\n\n```sql\nSELECT start_station_name, end_station_name, COUNT(*) AS count\nFROM trips_cleaned\nWHERE member_casual = 'casual'\nGROUP BY start_station_name, end_station_name\nORDER BY count DESC\n    LIMIT 10;\n\nSELECT start_station_name, end_station_name, COUNT(*) AS count\nFROM trips_cleaned\nWHERE member_casual = 'member'\nGROUP BY start_station_name, end_station_name\nORDER BY count DESC\n    LIMIT 10;\n```\n\nMost popular common trips. There is only 1: ellis ave \u0026 60th st to ellis ave \u0026 55th st.\n\n```sql\nWITH casual_trips AS (SELECT start_station_name, end_station_name, COUNT(*) AS count\nFROM trips_cleaned\nWHERE member_casual = 'casual'\nGROUP BY start_station_name, end_station_name\nORDER BY count DESC\n    LIMIT 10\n    ),\n    member_trips AS (\nSELECT start_station_name, end_station_name, COUNT (*) AS count\nFROM trips_cleaned\nWHERE member_casual = 'member'\nGROUP BY start_station_name, end_station_name\nORDER BY count DESC\n    LIMIT 10\n    )\nSELECT casual_trips.start_station_name, casual_trips.end_station_name\nFROM casual_trips\n         INNER JOIN member_trips\n                    ON casual_trips.start_station_name = member_trips.start_station_name\n                        AND casual_trips.end_station_name = member_trips.end_station_name;\n```\n\n### Casuals vs. Members: Average Ride Duration and Distance\n\nAverage ride duration for casuals vs. members (i.e. 50% of users in each category exhibit this behaviour).\n\n```sql\nSELECT AVG(TIMESTAMPDIFF(MINUTE, started_at, ended_at)) AS avg_ride_duration_mins_casual\nFROM trips_cleaned\nWHERE member_casual = 'casual';\n-- ~22 minutes\n\nSELECT AVG(TIMESTAMPDIFF(MINUTE, started_at, ended_at)) AS avg_ride_duration_mins_member\nFROM trips_cleaned\nWHERE member_casual = 'member';\n-- ~12 minutes\n```\n\nAverage ride distance for casuals vs. members (i.e. 50% of users in each category exhibit this behaviour).\n\n```sql\n-- Inputting a function to compute the distance between two points in km using latitude and longitude data. \nDELIMITER\n$$\n\nCREATE FUNCTION haversine_distance(lat1 FLOAT, lon1 FLOAT, lat2 FLOAT, lon2 FLOAT)\n    RETURNS FLOAT\n    DETERMINISTIC\nBEGIN\n    DECLARE\nR INTEGER DEFAULT 6371;  -- Radius of the Earth in kilometers\n    DECLARE\nlat1_rad FLOAT;\n    DECLARE\nlon1_rad FLOAT;\n    DECLARE\nlat2_rad FLOAT;\n    DECLARE\nlon2_rad FLOAT;\n    DECLARE\ndlat FLOAT;\n    DECLARE\ndlon FLOAT;\n    DECLARE\na FLOAT;\n    DECLARE\nc FLOAT;\n    DECLARE\ndistance FLOAT;\n\n    -- Convert degrees to radians\n    SET\nlat1_rad = RADIANS(lat1);\n    SET\nlon1_rad = RADIANS(lon1);\n    SET\nlat2_rad = RADIANS(lat2);\n    SET\nlon2_rad = RADIANS(lon2);\n\n    -- Calculate differences\n    SET\ndlat = lat2_rad - lat1_rad;\n    SET\ndlon = lon2_rad - lon1_rad;\n\n    -- Apply the Haversine formula\n    SET\na = SIN(dlat / 2) * SIN(dlat / 2) + COS(lat1_rad) * COS(lat2_rad) * SIN(dlon / 2) * SIN(dlon / 2);\n    SET\nc = 2 * ATAN2(SQRT(a), SQRT(1 - a));\n\n    -- Calculate the distance\n    SET\ndistance = R * c;\n\nRETURN distance; -- Distance in kilometers\nEND$$\n\nDELIMITER ;\n\n-- applying the function for our use case. \nSELECT AVG(haversine_distance(start_lat, start_lng, end_lat, end_lng)) AS avg_distance_in_km\nFROM trips_cleaned\nWHERE member_casual = 'casual';\n-- ~2 km\n\nSELECT AVG(haversine_distance(start_lat, start_lng, end_lat, end_lng)) AS avg_distance_in_km\nFROM trips_cleaned\nWHERE member_casual = 'member';\n-- ~2 km\n```\n\n## 4.3 Trip Timing Trends Across Month, Day, and Hour\n\n### Casuals vs. Members: Trip Timing Patterns by Month, Day, and Hour\n\nMonthly ride patterns for casuals vs. members.\n\n```sql\nSELECT EXTRACT(MONTH from started_at) as month,\n       COUNT(ride_id) as trips_per_month\nFROM trips_cleaned\nWHERE member_casual = 'casual'\nGROUP BY month\nORDER BY trips_per_month DESC;\n\nSELECT EXTRACT(MONTH from started_at) as month,\n       COUNT(ride_id) as trips_per_month\nFROM trips_cleaned\nWHERE member_casual = 'member'\nGROUP BY month\nORDER BY trips_per_month DESC;\n```\n\nDaily ride patterns for casuals vs. members.\n\n```sql\nSELECT DAYNAME(started_at) as day_of_week,\n       COUNT(ride_id)      as trips_per_day_of_week\nFROM trips_cleaned\nWHERE member_casual = 'casual'\nGROUP BY day_of_week\nORDER BY trips_per_day_of_week DESC;\n\nSELECT DAYNAME(started_at) as day_of_week,\n       COUNT(ride_id)      as trips_per_day_of_week\nFROM trips_cleaned\nWHERE member_casual = 'member'\nGROUP BY day_of_week\nORDER BY trips_per_day_of_week DESC;\n```\n\nHourly ride patterns for casuals vs. members.\n\n```sql\nSELECT HOUR (started_at) as hour_of_day, COUNT (ride_id) as trips_per_hour_of_day\nFROM trips_cleaned\nWHERE member_casual = 'casual'\nGROUP BY hour_of_day\nORDER BY trips_per_hour_of_day DESC;\n\nSELECT HOUR (started_at) as hour_of_day, COUNT (ride_id) as trips_per_hour_of_day\nFROM trips_cleaned\nWHERE member_casual = 'member'\nGROUP BY hour_of_day\nORDER BY trips_per_hour_of_day DESC;\n```\n\n\u003c/details\u003e\n\n# Phase 5: Share\n\n\u003cdetails\u003e\n  \u003csummary\u003ePresenting findings through Tableau visualizations to make insights accessible and actionable.\u003c/summary\u003e\n\n## Distribution of Casuals vs. Members\n\nLet's begin by breaking down casuals and members in our dataset. We see that there are ~2.8 million members and ~1.5 million casuals.\n\n\u003cdiv align=\"center\"\u003e\n\u003cimg width=\"201\" alt=\"1\" src=\"https://github.com/user-attachments/assets/34cd8708-3f1a-4311-bb5f-dc955f9f5a1d\"\u003e\n\u003c/div\u003e\n\n\u003cdiv align=\"center\"\u003e\n\u003cimg width=\"302\" alt=\"2\" src=\"https://github.com/user-attachments/assets/b0a67d06-8481-4ede-874d-9790a5c98932\"\u003e\n\u003c/div\u003e\n\n## Most and Least Used Bike Types\n\nClassic bikes are more popular than electric bikes across both categories, while docked bikes are only used by casuals.\n\n\u003cdiv align=\"center\"\u003e\n\u003cimg width=\"421\" alt=\"3\" src=\"https://github.com/user-attachments/assets/d5bffcbf-fa0b-4d20-8451-ba1230973bb2\"\u003e\n\u003c/div\u003e\n\n## Top 10 Start and End Stations \n\nLooking at the most popular start and end stations for casual riders, here are my key insights: \n\n**Tourist and Scenic Spots:** Many locations are well-known scenic or tourist areas in Chicago, such as Streeter Dr \u0026 Grand Ave (near Navy Pier and Lake Michigan), Millennium Park, Shedd Aquarium, Theater on the Lake, and Adler Planetarium. This suggests that casual riders may be tourists or people exploring popular sights.\n\n**Lakefront Locations:** Several stations are located along or near Lake Michigan, including DuSable Lake Shore Dr \u0026 Monroe St, Michigan Ave \u0026 Oak St, and Montrose Harbor, indicating that casual riders are drawn to scenic lakefront routes.\n\n**Recreational Areas:** High usage at stations near parks and recreational spots, like Millennium Park and DuSable Harbor, supports the idea that casual riders often use bikes for leisure rather than commuting\n\n**Round Trips:** The overlap between popular start and end locations suggests casual riders frequently take round trips, likely for sightseeing or short rides that begin and end near major attractions.\n\nOverall, these patterns indicate that casual riders primarily use the bike-sharing service for leisure and sightseeing, particularly around popular attractions and the lakefront, rather than for daily commuting.\n\n\u003cdiv align=\"center\"\u003e\n\u003cimg width=\"859\" alt=\"image\" src=\"https://github.com/user-attachments/assets/0987abab-31a3-49b6-b81b-4ed06b29bce5\"\u003e\n\u003c/div\u003e\n\n\u003cbr /\u003e Looking at the most popular start and end stations for members, here are my key insights: \u003cbr /\u003e \n\n**Downtown Locations:** Many stations, such as Clinton St \u0026 Washington Blvd, Kingsbury St \u0026 Kinzie St, Clark St \u0026 Elm St, and Clinton St \u0026 Madison St, are near key intersections in busy downtown areas. This suggests that member riders are likely using bike-sharing for commuting or accessing frequently visited spots in the city center, such as offices.\n\n**University and Residential Areas:** Stations like University Ave \u0026 57th St, Loomis St \u0026 Lexington St, and Ellis Ave \u0026 60th St suggest that some members may be students or residents who use bike-sharing regularly within their neighbourhoods or for commuting to nearby facilities.\n\n**Broader Distribution Across Residential, Commercial, and Practical Locations:** Unlike casual riders, who tend to cluster around tourist-heavy areas, member trips are spread across a wider range of residential and commercial locations. This indicates that members prioritize practical locations closer to workplaces, residences, and transit hubs, highlighting a focus on commuting and utility trips rather than leisure or tourism.\n\n**Consistent Start and End Patterns:** Similar to casual riders, the overlap between popular start and end stations suggests that members often take round trips or short point-to-point rides within the same area, which aligns with typical commuting behaviour.\n\nOverall, these patterns indicate that members primarily use the bike-sharing service for commuting to work or school or for routine travel within the city, focusing on practical and accessible locations over tourist destinations or scenic spots.\n\n\u003cdiv align=\"center\"\u003e\n\u003cimg width=\"859\" alt=\"image\" src=\"https://github.com/user-attachments/assets/7653ce55-6912-46dc-92c4-a911209bf27a\"\u003e\n\u003c/div\u003e\n\n## Average Ride Durations and Distances\n\nLooking at the average ride duration and distance for casual riders vs. members, we observe that casual riders, on average, use the service twice as long as members; however, the actual distance covered is comparable. This suggests that casual riders are likelier to take long, leisurely trips, possibly for sightseeing or recreation. In contrast, members use the service more efficiently, taking shorter, practical trips like commuting.\n\nIn summary, this pattern indicates that casual riders use the service for leisure-oriented, extended rides, while members prioritize efficiency, consistent with a commuting or task-focused approach to bike-sharing.\n\n\u003cdiv align=\"center\"\u003e\n\u003cimg width=\"529\" alt=\"6\" src=\"https://github.com/user-attachments/assets/62e99864-3f60-4ada-a388-f54b78c67ce6\"\u003e\n\u003c/div\u003e\n\n## Trip Timing Patterns by Month, Day, and Hour\n\nObserving trip timing patterns by month, day, and hour, here are my key insights:\n\n**Monthly Trends:** Both casual and member riders show increased activity in warmer months, peaking from May to September. Casual riders exhibit a more pronounced summer peak, especially around July, suggesting they use the service primarily for leisure or tourism, which is more seasonal. In contrast, member usage remains relatively steady year-round, with only a slight increase in summer, indicating consistent use likely for commuting or regular transportation needs.\n\n**Daily Trends:** Casual riders prefer weekends, especially Saturdays, while members have steady weekday usage with minor fluctuations. This pattern reinforces the idea that casual users are primarily engaging in leisure or recreational rides on weekends, whereas members’ consistent weekday usage aligns with commuting or routine trips.\n\n**Hourly Trends:** Casual riders’ trips peak in the afternoon (3 PM - 5 PM), suggesting a preference for leisurely rides during those hours. Member riders show two distinct peaks: one around 8 AM and another around 5 PM, typical of commuting patterns as users ride to and from work during rush hours. Both groups have significantly lower activity late at night, indicating the service is primarily used during daytime and evening hours.\n\nOverall, these patterns indicate that casual riders use the bike-sharing service seasonally, favouring summer months, weekends, and afternoons, reflecting a leisure-oriented use. Member riders display a more consistent, year-round pattern, with weekday peaks during commute hours, suggesting practical, task-focused usage.\n\n\u003cdiv align=\"center\"\u003e\n\u003cimg width=\"874\" alt=\"7\" src=\"https://github.com/user-attachments/assets/c8763de8-9cd3-4bfd-b5a4-1119161cae77\"\u003e\n\u003c/div\u003e\n\n\u003c/details\u003e\n\n# Phase 6: Act \n\n\u003cdetails\u003e\n  \u003csummary\u003eReporting the results of the analysis to project stakeholders and providing recommendations to address the business problem.\u003c/summary\u003e\n    \n\u003cbr /\u003e Based on our findings on casual and member rider patterns, here are targeted marketing strategies to encourage casual riders to become members: \u003cbr /\u003e\n\n**Seasonal Promotions During Peak Months:** Casual riders are most active in the summer, so offering limited-time discounts or promotional rates for new memberships during these months (e.g., 20% off if they sign up in July) can capitalize on when they’re most engaged with the service. Summer-only perks like free ride credits or priority access to busy stations can further incentivize them to join.\n\n**Weekend-Exclusive Membership Benefits:** Since casual riders favour weekend rides, create a “Weekend Warrior” membership option that includes benefits such as extra ride time or priority access to high-demand stations on weekends. Emphasize the cost savings for frequent weekend usage, making the membership appealing to those who primarily ride on weekends.\n\n**Cost-Comparison Campaigns Near Popular Tourist and Leisure Spots:** Many casual riders may not realize the cost benefits of a membership. Targeted ads at popular casual rider locations (like Streeter Dr, Millennium Park, and Shedd Aquarium) can showcase how a membership helps avoid per-ride fees, which is ideal for those exploring the city. Partnering with local attractions or events near these stations to offer temporary discounts or “day passes” with an upgrade option can also boost membership interest.\n\n**Free Trial or Flexible Membership Options for Leisure Riders:** Providing a one-week or one-month trial during summer allows casual riders to experience the benefits of having a membership with no risk. Offering a discounted first month after the trial could encourage them to stay. Alternatively, short-term memberships with flexible terms, such as pausing or cancelling in off-peak seasons, can attract riders who don’t want a year-round commitment.\n\n**Incentives for Longer Rides:** Since casual riders tend to enjoy scenic, longer rides, offer rewards for completing rides over a certain distance or duration (e.g., 5 km or 20 minutes), reinforcing the value of a membership for those seeking leisurely experiences.\n\nThese strategies leverage casual riders’ seasonal and weekend preferences and appeal to their potential for more frequent use while reducing the perceived risk of commitment.\n\n\u003c/details\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdcostachar%2Fcyclistic-case-study","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdcostachar%2Fcyclistic-case-study","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdcostachar%2Fcyclistic-case-study/lists"}