├── .eslintrc.js ├── .gitignore ├── Contributing.md ├── LICENSE.md ├── README.md ├── Roadmap.md ├── package.json ├── public ├── CNAME ├── DataClinicLogo.png ├── SubwayCrowds.png ├── average_crowding_by_hour.csv ├── cars_by_line.csv ├── crowding_by_weekday_direction.csv ├── crowding_methodology.svg ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt ├── site-logo.png ├── social-card.png ├── stops.csv └── timestamp.txt ├── scripts ├── Crowding.ipynb ├── cleaned_crosswalk.csv ├── crowding.py ├── data │ ├── .ipynb_checkpoints │ │ ├── cleaned_crosswalk-checkpoint.csv │ │ └── cross_walks-checkpoint.csv │ ├── Master_crosswalk.csv │ ├── cleaned_crosswalk.csv │ ├── cross_walks.csv │ ├── elevator_to_line_dir_station.csv │ ├── google_transit │ │ ├── agency.txt │ │ ├── calendar.txt │ │ ├── calendar_dates.txt │ │ ├── routes.txt │ │ ├── stops.txt │ │ ├── transfers.txt │ │ └── trips.txt │ ├── station_to_station.csv │ ├── stops.csv │ └── stops.txt ├── gcs_utils.py ├── gtfs.py ├── heuristics.py ├── make_stops_for_line.py ├── requirements.txt └── turnstile.py ├── src ├── App.scss ├── App.test.tsx ├── App.tsx ├── AppSyles.tsx ├── Contexts │ └── DataContext.tsx ├── Hooks │ ├── useCrowdingData.ts │ ├── useStationsForLine.ts │ └── useStopsBetween.ts ├── components │ ├── AboutModal │ │ ├── AboutModal.tsx │ │ └── AboutModalStyles.tsx │ ├── DayOfWeekSelector │ │ ├── DayOfWeekSelector.tsx │ │ └── DayOfWeekSelectorStyles.tsx │ ├── FeedbackModal │ │ ├── FeedbackModal.tsx │ │ └── FeedbackModalStyles.tsx │ ├── HourSlider │ │ ├── HourSlider.tsx │ │ └── HourSliderStyles.tsx │ ├── HourlyChart │ │ └── HourlyChart.tsx │ ├── MetricSelector │ │ ├── MetricSelector.tsx │ │ └── MetricSelectorStyles.tsx │ ├── SentanceDropDown │ │ ├── SentanceDropDown.tsx │ │ └── SentanceDropDownStyles.tsx │ ├── ShareButtons │ │ └── ShareButtons.tsx │ ├── SimplePassword │ │ ├── SimplePassword.tsx │ │ └── SimplePasswordStyles.tsx │ ├── StopsChart │ │ ├── StopsChart.tsx │ │ └── StopsChartStyles.tsx │ ├── ToggleButton │ │ ├── ToggleButton.tsx │ │ └── ToggleButtonStyles.tsx │ └── TopBar │ │ ├── TopBar.tsx │ │ └── TopBarStyles.tsx ├── icons │ ├── giticon.png │ └── mediumicon.png ├── index.css ├── index.tsx ├── logo.svg ├── react-app-env.d.ts ├── serviceWorker.ts ├── setupTests.ts ├── types.tsx └── utils.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | 12 | ], 13 | "globals": { 14 | "Atomics": "readonly", 15 | "SharedArrayBuffer": "readonly" 16 | }, 17 | "parser": "@typescript-eslint/parser", 18 | "parserOptions": { 19 | "ecmaFeatures": { 20 | "jsx": true 21 | }, 22 | "ecmaVersion": 2018, 23 | "sourceType": "module" 24 | }, 25 | "plugins": [ 26 | "react", 27 | "@typescript-eslint" 28 | ], 29 | "rules": { 30 | } 31 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .ipynb_checkpoints/ 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /Contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | ## Naming Branches 4 | Please name all branches with the following format: 5 | 6 | ``` 7 | {type}/{GitHub Issue Number}-{short}-{sentence} 8 | ``` 9 | 10 | Examples: 11 | ``` 12 | feature/1-Adding-Download-Button 13 | bugfix/2-Fixing-Feedback-Screen 14 | task/3-Refactoring-Methodology 15 | task/NOISSUE-update-react-version 16 | ``` 17 | 18 | Types: Can be feature, bugfix, or task. 19 | ## Naming Commits 20 | Please describe all commits with this format: 21 | 22 | {GitHub Issue # or NOISSUE} {Some short description} 23 | 24 | Examples: 25 | 26 | Issue #3 Add a button 27 | NOISSUE Fix NullPointerException 28 | 29 | 30 | ## Separating Concerns 31 | - Housekeeping tasks (eg bumping a version of a dependency, adding autogenerated code) should be kept in a separate commit, and preferably in a separate and dedicated PR. 32 | 33 | - Where possible, try to keep pure refactoring (rearranging source code without detectable behavior changes) in a separate commit and PR from other changes. This makes them easier to review. 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This project and corresponding website are no longer maintained by Two Sigma. We continue to encourage independent development.** 2 | 3 | [![Netlify Status](https://api.netlify.com/api/v1/badges/6abecf5a-8e9f-4b9e-818d-ed47c21ef863/deploy-status)](https://app.netlify.com/sites/howbusyismytrain/deploys) 4 | 5 |

6 | 7 |

8 | 9 | # Subway Crowds 10 | 11 |

12 | 13 |

14 | 15 | ### Plan your commute better 16 | We built SubwayCrowds to estimate how crowded your subway trip is likely to be. 17 | 18 | As the city continues to adjust to the new normal and people begin heading back to work and school, a central question is how will this work given NYC commuters reliance on public transportation. Is it possible to move so many people while maintaining social distancing? To help inform this question, SubwayCrowds is designed to identify for specific trips when subway cars are likely to be most crowded so that individuals might alter their travel time or route. 19 | 20 | 21 | ### Methodology 22 | 23 |

24 | 25 |

26 | 27 | 28 | **1. Cleaning Schedule Data** 29 | - Concatenate GTFS data pulled every minute for a given time range 30 | - Drop duplicates (on start date, time, trip_id, station) so we don’t double count the same train in the same station twice, and keep the latest time the data is available for each train reaching each station 31 | - Infer starting time of trip in cases where its missing by identifying the earliest available information on that trip 32 | - Define new unique ID for trips (as trip IDs used in raw data repeat across days) 33 | - For each trip, fill in the starting station if missing (this happens a lot) to be 2 minutes before the first stop on the route. 34 | - Calculate length of each trip and exclude trips < 25% of max trip length 35 | 36 | 37 | **2. Cleaning & merging Turnstile Data** 38 | - Aggregate total entries and exits by Station and timestamp, consolidating counts for stations with multiple turnstiles (eg. Times Square, Penn station) 39 | - Exclude rows with wild jumps in counts (negative or >10000 in 4hrs) 40 | - Quadratic interpolation of cumulative counts to every minute 41 | - Correct for interpolation bias around peak and lean hours 42 | - Set count at 6am to be 10% of the count from 4am-8am 43 | - Set count at 9am to be 40% of the count from 8am-12pm 44 | - Merge with schedule data 45 | - For each train arrival, calculate total entries and exits since the last train at the station 46 | 47 | 48 | **3. Trip assignment & heuristics** 49 | - For a given line and direction at station for an hour to approximate which direction a person goes when entering a station we use: 50 | - *Entry weight = 1 - cumulative exits along route **after this station** at this hour / total exits along the route **in either direction** at this hour* 51 | - *Exit weight = 1 - entry weight* 52 | - Normalize weights as a proportion of all the lines in the station 53 | - Find service changes in the schedule and impute weights for these as the average for that station (to handle cases like C train running along F line) 54 | 55 | 56 | **4. Crowding Estimation** 57 | - For the first train of the day (around 5am), we set people waiting at the station to 0 (Stations are meant to be closed between 1 and 5 am, yet we see a few entries in the station between these hours) 58 | - We define entry_exit_ratio as the average daily ratio between overall entries and exits (typically between 1.2 and 1.4) to account for individuals exiting the station through the emergency exits (we use 1.25 currently) 59 | - For each stop, we calculate the following: (initialized to 0) 60 | - *waiting[t] = waiting[t-1] - train_entries[t-1] + total_entries_since_last_train* 61 | - *train_entries[t] = waiting[t] **x** entry_weight* 62 | - *train_exits[t] = min(total_exits_before_next_train **x** entry_exit_ratio **x** exit_weight, crowd[t-1])* 63 | - *crowd[t] = crowd[t-1] - train_exits[t] + train_entries[t]* 64 | - Aggregate estimates for each hour for each line and station 65 | 66 | 67 | ### Developing 68 | 69 | Thank you developing this tool with us! before you start, do checkout our [Roadmap](Roadmap.md) and [Contributing guidelines](Contributing.md). 70 | 71 | To develop the web-app locally, run 72 | 73 | ```bash 74 | yarn 75 | yarn start 76 | ``` 77 | To set up the python environment, run 78 | 79 | ```bash 80 | conda config --append channels conda-forge 81 | conda create -n {env_name} --file scripts/requirements.txt 82 | 83 | ## to have the environment showup as a kernel on jupyter 84 | python -m ipykernel install --user --name {env_name} --display-name "Python ({env_name})" 85 | ``` 86 | 87 | To generate crowd estimates, edit global variables at the top and run 88 | 89 | ```bash 90 | python scripts/crowding.py 91 | ``` 92 | 93 | ### Directory Structure 94 | 95 | SubwayCrowds/ 96 | ├── LICENSE 97 | ├── README.md <- The top-level README for developers using this project 98 | │ 99 | ├── scripts 100 | │ ├── data <- Other data used for crowding estimation such as crosswalks, GTFS static schedule, etc. 101 | │ ├── gcs_utils.py <- Utility functions for accessing data from Google Cloud Storage bucket 102 | │ ├── gtfs.py <- Processing real-time gtfs data 103 | │ ├── tunrstile.py <- Cleaning and interpolating turnstile data 104 | │ ├── heuristics.py <- Logic for trip assignment and crowd estimation 105 | │ ├── crowding.py <- Pipeline for generating subway crowd estimates 106 | │ └── Crowding.ipynb <- Notebook version of crowding.py 107 | | 108 | ├── public <- Static files used by the application 109 | │ 110 | ├── requirements.txt <- Packages to build the python environment 111 | │ 112 | ├── src <- React front-end application structure 113 | │ ├── Context 114 | │ ├── Hooks 115 | │ └── components 116 | 117 | 118 | ### Datasets used 119 | 120 | The links to the data used to generate crowd estimates are below: 121 | 122 | - GTFS Realtime Feed 123 | - GTFS Static Data 124 | - Turnstile Usage Data 125 | - Crosswalk to merge GTFS and Turnstile data, Standard stop order, etc. 126 | 127 | -------------------------------------------------------------------------------- /Roadmap.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | This document outlines the roadmap for development of SubwayCrowds. The aim is to keep this populated with a set of planned tasks and features. 4 | 5 | If you would like to request a new feature or prioritize one over the others, open a github issue and we can discuss further. 6 | 7 | ## New features 8 | - A low touch feedback process to get more feedback from users. 9 | - Easily switch between alternate train options. For example: If the selected trip is covered by A, C, and E trains, make it easier for user to compare the crowd on each. 10 | - Ability for the user to add multiple stops that may include transfers so they can view crowd estimates for the comple commute in one go. For example: 2 Line => 96th St -> Times Square followed by R Line => Times Square -> 8th St NYU 11 | 12 | 13 | ## Exploratory 14 | - Explore potential improvements to crowd estimating methodology. (ex: as a Max-flow model on a graph) 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mta_crowding_interactive", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@dataclinic/about-page": "^0.3.5", 7 | "@dataclinic/icons": "^0.3.5", 8 | "@dataclinic/theme": "^0.3.5", 9 | "@dataclinic/typography": "^0.3.5", 10 | "@fortawesome/fontawesome-svg-core": "^1.2.30", 11 | "@fortawesome/free-solid-svg-icons": "^5.14.0", 12 | "@fortawesome/react-fontawesome": "fortawesome/react-fontawesome", 13 | "@material-ui/core": "^4.11.0", 14 | "@testing-library/jest-dom": "^4.2.4", 15 | "@testing-library/react": "^9.3.2", 16 | "@testing-library/user-event": "^7.1.2", 17 | "@types/chartjs-plugin-annotation": "^0.5.0", 18 | "@types/jest": "^24.0.0", 19 | "@types/node": "^12.0.0", 20 | "@types/react": "^16.9.0", 21 | "@types/react-dom": "^16.9.0", 22 | "@types/react-modal": "^3.10.6", 23 | "chart.js": "^2.9.3", 24 | "chartjs-plugin-annotation": "^0.5.7", 25 | "fathom-client": "^3.0.0", 26 | "gh-pages": "^2.2.0", 27 | "md5": "^2.3.0", 28 | "papaparse": "^5.2.0", 29 | "query-string": "^6.13.1", 30 | "react": "^16.13.1", 31 | "react-chartjs-2": "^2.9.0", 32 | "react-dom": "^16.13.1", 33 | "react-input-slider": "^6.0.0", 34 | "react-modal": "^3.11.2", 35 | "react-scripts": "3.4.1", 36 | "react-share": "^4.2.1", 37 | "react-tooltip": "^4.2.10", 38 | "react-use-dimensions": "^1.2.1", 39 | "styled-components": "^5.1.1", 40 | "typeface-lato": "^0.0.75", 41 | "typescript": "~3.7.2", 42 | "use-media": "^1.4.0" 43 | }, 44 | "husky": { 45 | "hooks": { 46 | "pre-commit": "lint-staged" 47 | } 48 | }, 49 | "lint-staged": { 50 | "./src/*.{js,jsx,ts,tsx}": [ 51 | "npx prettier --write", 52 | "eslint . --ext .ts" 53 | ] 54 | }, 55 | "scripts": { 56 | "start": "react-scripts start", 57 | "build": "CI=\"\" react-scripts build", 58 | "test": "react-scripts test", 59 | "eject": "react-scripts eject", 60 | "predeploy": "yarn build", 61 | "deploy": "gh-pages -d build" 62 | }, 63 | "browserslist": { 64 | "production": [ 65 | ">0.2%", 66 | "not dead", 67 | "not op_mini all" 68 | ], 69 | "development": [ 70 | "last 1 chrome version", 71 | "last 1 firefox version", 72 | "last 1 safari version" 73 | ] 74 | }, 75 | "devDependencies": { 76 | "@types/md5": "^2.2.0", 77 | "@types/papaparse": "^5.0.3", 78 | "@typescript-eslint/eslint-plugin": "^3.8.0", 79 | "@typescript-eslint/parser": "^3.8.0", 80 | "eslint-config-airbnb": "^18.2.0", 81 | "eslint-plugin-react": "^7.20.5", 82 | "husky": "^4.2.5", 83 | "lint-staged": "^10.2.11", 84 | "node-sass": "^4.14.1", 85 | "prettier": "^2.0.5" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /public/CNAME: -------------------------------------------------------------------------------- 1 | mtacrowding.tsdataclinic.com 2 | -------------------------------------------------------------------------------- /public/DataClinicLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsdataclinic/SubwayCrowds/759994571546349024c107c339a489e14a7439d4/public/DataClinicLogo.png -------------------------------------------------------------------------------- /public/SubwayCrowds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsdataclinic/SubwayCrowds/759994571546349024c107c339a489e14a7439d4/public/SubwayCrowds.png -------------------------------------------------------------------------------- /public/cars_by_line.csv: -------------------------------------------------------------------------------- 1 | line,num_cars 2 | 1,9 3 | 2,9 4 | 3,9 5 | 4,9 6 | 5,9 7 | 6,9 8 | 7,11 9 | A,9 10 | B,9 11 | C,9 12 | D,9 13 | E,9 14 | F,9 15 | G,4 16 | J,9 17 | L,8 18 | M,9 19 | N,8 20 | Q,8 21 | R,8 22 | W,8 23 | Z,8 24 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsdataclinic/SubwayCrowds/759994571546349024c107c339a489e14a7439d4/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 41 | Subway Crowds 42 | 43 | 44 | 45 |
46 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsdataclinic/SubwayCrowds/759994571546349024c107c339a489e14a7439d4/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsdataclinic/SubwayCrowds/759994571546349024c107c339a489e14a7439d4/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/site-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsdataclinic/SubwayCrowds/759994571546349024c107c339a489e14a7439d4/public/site-logo.png -------------------------------------------------------------------------------- /public/social-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsdataclinic/SubwayCrowds/759994571546349024c107c339a489e14a7439d4/public/social-card.png -------------------------------------------------------------------------------- /public/timestamp.txt: -------------------------------------------------------------------------------- 1 | 20220521-20220527 -------------------------------------------------------------------------------- /scripts/crowding.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import sys, logging 4 | from datetime import datetime, timedelta 5 | import time 6 | import matplotlib.pyplot as plt 7 | import re 8 | import os 9 | import turnstile 10 | import gtfs 11 | import heuristics 12 | 13 | root = logging.getLogger() 14 | root.setLevel(logging.INFO) 15 | 16 | handler = logging.StreamHandler(sys.stdout) 17 | handler.setLevel(logging.DEBUG) 18 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 19 | handler.setFormatter(formatter) 20 | root.addHandler(handler) 21 | 22 | from gcs_utils import gcs_util 23 | gcs = gcs_util(bucket_path='mta_crowding_data') 24 | 25 | ################################################## 26 | ## Global variables 27 | 28 | LAST_TURNSTILE_DATE = datetime(year=2020,month=11,day=7) 29 | DAYS_RANGE = 14 30 | 31 | ################################################## 32 | 33 | ## Loading pre-requisite files 34 | stop_order_merged = pd.read_csv('scripts/data/stops.csv') 35 | crosswalk = pd.read_csv('scripts/data/Master_crosswalk.csv') 36 | station_to_station = pd.read_csv('scripts/data/station_to_station.csv') 37 | 38 | ## defining date range for estimation 39 | dates = [LAST_TURNSTILE_DATE - timedelta(days=i) for i in range(1,DAYS_RANGE +1)] 40 | keep_dates = [re.sub('-','',str(d.date())) for d in dates] 41 | 42 | ## proceesing real-time GTFS data 43 | df, dates = gtfs.get_schedule(LAST_TURNSTILE_DATE,DAYS_RANGE) 44 | clean_df = gtfs.process_schedule(df, min(dates), max(dates), stop_order_merged) 45 | 46 | ## proceesing static GTFS data 47 | static_schedule = pd.read_csv('scripts/data/google_transit/stop_times.txt') 48 | trips = pd.read_csv('scripts/data/google_transit/trips.txt') 49 | stops = pd.read_csv('scripts/data/google_transit/stops.txt') 50 | static_schedule = static_schedule.merge(trips,on='trip_id').merge(stops,on='stop_id') 51 | 52 | last_year_start = min(dates) - timedelta(weeks=52) 53 | last_year_end = max(dates) - timedelta(weeks=52) 54 | 55 | clean_static_schedule = gtfs.process_static_schedule(static_schedule,last_year_start, last_year_end) 56 | 57 | ## Processing Turnstile data 58 | 59 | ## Current 60 | turnstile_data_raw = turnstile._process_raw_data(turnstile.download_turnstile_data(start_date=min(dates), end_date=max(dates)), group_by=['STATION','LINENAME','UNIT']) 61 | turnstile_data_raw_imputed = turnstile.pre_interpolation_fix(turnstile_data_raw) 62 | turnstile_data_raw_imputed = turnstile_data_raw_imputed.set_index('datetime') 63 | turnstile_data = turnstile._interpolate(turnstile_data_raw_imputed, group_by=['STATION','LINENAME','UNIT'], frequency='1T') 64 | turnstile_data = turnstile_data[turnstile_data.index.to_series().between(min(dates), max(dates))] .drop(columns=["entry_diffs", "exit_diffs"]) 65 | turnstile_data_cleaned = turnstile.consolidate_turnstile_data(turnstile_data) 66 | 67 | ## Last Month 68 | last_month_start = min(dates) - timedelta(weeks=4) 69 | last_month_end = max(dates) - timedelta(weeks=4) 70 | turnstile_data_raw = turnstile._process_raw_data(turnstile.download_turnstile_data(start_date=last_month_start, end_date=last_month_end), group_by=['STATION','LINENAME','UNIT']) 71 | turnstile_data_raw_imputed = turnstile.pre_interpolation_fix(turnstile_data_raw) 72 | turnstile_data_raw_imputed = turnstile_data_raw_imputed.set_index('datetime') 73 | last_month_turnstile_data = turnstile._interpolate(turnstile_data_raw_imputed, group_by=['STATION','LINENAME','UNIT'], frequency='1T') 74 | last_month_turnstile_data = last_month_turnstile_data[last_month_turnstile_data.index.to_series().between(last_month_start, last_month_end)] .drop(columns=["entry_diffs", "exit_diffs"]) 75 | last_month_turnstile_data = last_month_turnstile_data.reset_index() 76 | last_month_turnstile_data['datetime'] = last_month_turnstile_data.datetime + timedelta(weeks=4) 77 | last_month_turnstile_data = last_month_turnstile_data.set_index('datetime') 78 | last_month_turnstile_data_cleaned = turnstile.consolidate_turnstile_data(last_month_turnstile_data) 79 | 80 | ## Last Year 81 | last_year_start = min(dates) - timedelta(weeks=52) 82 | last_year_end = max(dates) - timedelta(weeks=52) 83 | turnstile_data_raw = turnstile._process_raw_data(turnstile.download_turnstile_data(start_date=last_year_start, end_date=last_year_end), group_by=['STATION','LINENAME','UNIT']) 84 | turnstile_data_raw_imputed = turnstile.pre_interpolation_fix(turnstile_data_raw) 85 | turnstile_data_raw_imputed = turnstile_data_raw_imputed.set_index('datetime') 86 | last_year_turnstile_data = turnstile._interpolate(turnstile_data_raw_imputed, group_by=['STATION','LINENAME','UNIT'], frequency='1T') 87 | last_year_turnstile_data = last_year_turnstile_data[last_year_turnstile_data.index.to_series().between(last_year_start, last_year_end)] .drop(columns=["entry_diffs", "exit_diffs"]) 88 | last_year_turnstile_data_cleaned = turnstile.consolidate_turnstile_data(last_year_turnstile_data) 89 | 90 | 91 | ## Merging GTFS and Turnstile data 92 | crosswalk = crosswalk[~(crosswalk.turnstile_station_name == '14TH STREET')] 93 | 94 | schedule = heuristics.get_schedule_with_weights(turnstile_data_cleaned,clean_df,stop_order_merged) 95 | schedule_last_month = heuristics.get_schedule_with_weights(last_month_turnstile_data_cleaned,clean_df,stop_order_merged) 96 | schedule_last_year = heuristics.get_schedule_with_weights(last_year_turnstile_data_cleaned,clean_static_schedule,stop_order_merged) 97 | 98 | df_merge = heuristics.merge_turnstile_schedule(turnstile_data_cleaned,crosswalk, schedule) 99 | last_month_df_merge = heuristics.merge_turnstile_schedule(last_month_turnstile_data_cleaned,crosswalk, schedule_last_month) 100 | last_year_df_merge = heuristics.merge_turnstile_schedule(last_year_turnstile_data_cleaned,crosswalk, schedule_last_year) 101 | 102 | 103 | ## Crowd estimation 104 | crowd_by_station_line = heuristics.get_crowd_by_station_line(df_merge,entry_exit_ratio=1.25) 105 | last_month_crowd_by_station_line = heuristics.get_crowd_by_station_line(last_month_df_merge,entry_exit_ratio=1.25) 106 | last_year_crowd_by_station_line = heuristics.get_crowd_by_station_line(last_year_df_merge,entry_exit_ratio=1.25) 107 | 108 | 109 | ## Aggregating estimates by hour and weekday/weekend 110 | station_to_station['from'] = [re.sub(r'N$|S$','',x) for x in station_to_station['from']] 111 | station_to_station['to'] = [re.sub(r'N$|S$','',x) for x in station_to_station['to']] 112 | clean_stop_routes = station_to_station[['from','line']].rename(columns={'from':'stop_id'}) 113 | clean_stop_routes = clean_stop_routes.append(station_to_station[['to','line']].rename(columns={'to':'stop_id'})) 114 | clean_stop_routes = clean_stop_routes.drop_duplicates() 115 | clean_stop_routes = clean_stop_routes[~clean_stop_routes.line.isin(['H','SI'])] 116 | clean_stop_routes['route_stop'] = clean_stop_routes['line'] + '_' + clean_stop_routes['stop_id'] 117 | 118 | current_avg, current_avg_split = heuristics.get_hourly_averages(crowd_by_station_line, clean_stop_routes) 119 | last_month_avg, last_month_avg_split = heuristics.get_hourly_averages(last_month_crowd_by_station_line, clean_stop_routes) 120 | last_year_avg, last_year_avg_split = heuristics.get_hourly_averages(last_year_crowd_by_station_line, clean_stop_routes) 121 | 122 | current_avg.rename(columns={'crowd':'current_crowd'}, inplace=True) 123 | last_month_avg.rename(columns={'crowd':'last_month_crowd'}, inplace=True) 124 | last_year_avg.rename(columns={'crowd':'last_year_crowd'}, inplace=True) 125 | 126 | hourly_average_estimates = current_avg.merge(last_month_avg,on=['STATION','route_id','hour']) 127 | hourly_average_estimates = hourly_average_estimates.merge(last_year_avg,on=['STATION','route_id','hour']) 128 | 129 | current_avg_split.rename(columns={'crowd':'current_crowd'}, inplace=True) 130 | last_month_avg_split.rename(columns={'crowd':'last_month_crowd'}, inplace=True) 131 | last_year_avg_split.rename(columns={'crowd':'last_year_crowd'}, inplace=True) 132 | 133 | hourly_average_estimates_split = current_avg_split.merge(last_month_avg_split,on=['STATION','route_id','hour','direction_id','weekday']) 134 | hourly_average_estimates_split = hourly_average_estimates_split.merge(last_year_avg_split,on=['STATION','route_id','hour','direction_id','weekday']) 135 | 136 | # hourly_average_estimates.to_csv('MTACrowdingInteractive/public/average_crowding_by_hour.csv',index=False) 137 | hourly_average_estimates_split.to_csv('public/crowding_by_weekday_direction.csv',index=False) 138 | 139 | 140 | timestr = min_st.strftime("%Y%m%d") 141 | timesen = max(dates).strftime("%Y%m%d") 142 | test_time = open('public/timestamp.txt', 'w') 143 | test_time.write(timestr+'-'+timesen) 144 | test_time.close() 145 | -------------------------------------------------------------------------------- /scripts/data/.ipynb_checkpoints/cross_walks-checkpoint.csv: -------------------------------------------------------------------------------- 1 | name,id 2 | "Van Cortlandt Park - 242 St",none 3 | "238 St","238 ST" 4 | "231 St","231 ST" 5 | "Marble Hill - 225 St","MARBLE HILL-225" 6 | "215 St","215 ST" 7 | "207 St","207 ST" 8 | "Dyckman St","DYCKMAN ST" 9 | "191 St","191 ST" 10 | "181 St","181 ST" 11 | "168 St - Washington Hts","168 ST" 12 | "157 St","157 ST" 13 | "145 St","145 ST" 14 | "137 St - City College","137 ST CITY COL" 15 | "125 St","125 ST" 16 | "116 St - Columbia University","116 ST-COLUMBIA" 17 | "Cathedral Pkwy (110 St)","CATHEDRAL PKWY" 18 | "103 St","103 ST" 19 | "96 St","96 ST" 20 | "86 St","86 ST" 21 | "79 St","79 ST" 22 | "72 St","72 ST" 23 | "66 St - Lincoln Center","66 ST-LINCOLN" 24 | "59 St - Columbus Circle","59 ST COLUMBUS" 25 | "50 St","50 ST" 26 | "Times Sq - 42 St","TIMES SQ-42 ST" 27 | "34 St - Penn Station","34 ST-PENN STA" 28 | "28 St","28 ST" 29 | "23 St","23 ST" 30 | "18 St","18 ST" 31 | "14 St","14 ST" 32 | "Christopher St - Sheridan Sq","CHRISTOPHER ST" 33 | "Houston St","HOUSTON ST" 34 | "Canal St","CANAL ST" 35 | "Franklin St","FRANKLIN AV" 36 | "Chambers St","CHAMBERS ST" 37 | "Cortlandt St","CORTLANDT ST" 38 | "Rector St","RECTOR ST" 39 | "South Ferry","SOUTH FERRY" 40 | "Wakefield - 241 St","WAKEFIELD/241" 41 | "Nereid Av","NEREID AV" 42 | "233 St","233 ST" 43 | "225 St","225 ST" 44 | "219 St","219 ST" 45 | "Gun Hill Rd","GUN HILL RD" 46 | "Burke Av","BURKE AV" 47 | "Allerton Av","ALLERTON AV" 48 | "Pelham Pkwy","PELHAM PKWY" 49 | "Bronx Park East","BRONX PARK EAST" 50 | "E 180 St","E 180 ST" 51 | "West Farms Sq - E Tremont Av","WEST FARMS SQ" 52 | "174 St","174 ST" 53 | "Freeman St","FREEMAN ST" 54 | "Simpson St","SIMPSON ST" 55 | "Intervale Av","INTERVALE AV" 56 | "Prospect Av","PROSPECT AV" 57 | "Jackson Av","JACKSON AV" 58 | "3 Av - 149 St","3 AV-149 ST" 59 | "149 St - Grand Concourse","149/GRAND CONC" 60 | "135 St","135 ST" 61 | "116 St","116 ST" 62 | "Central Park North (110 St)","CENTRAL PK N110" 63 | "Park Pl","PARK PLACE" 64 | "Fulton St","FULTON ST" 65 | "Wall St","WALL ST" 66 | "Clark St","CLARK ST" 67 | "Borough Hall","BOROUGH HALL" 68 | "Hoyt St","HOYT ST" 69 | "Nevins St","NEVINS ST" 70 | "Atlantic Av - Barclays Ctr","ATL AV-BARCLAY" 71 | "Bergen St","BERGEN ST" 72 | "Grand Army Plaza","GRAND ARMY PLAZ" 73 | "Eastern Pkwy - Brooklyn Museum",none 74 | "Franklin Av","FRANKLIN AV" 75 | "President St","PRESIDENT ST" 76 | "Sterling St","STERLING ST" 77 | "Winthrop St","WINTHROP ST" 78 | "Church Av","CHURCH AV" 79 | "Beverly Rd","BEVERLY RD" 80 | "Newkirk Av","NEWKIRK AV" 81 | "Flatbush Av - Brooklyn College","FLATBUSH AV-B.C" 82 | "Nostrand Av","NOSTRAND AV" 83 | "Kingston Av","KINGSTON AV" 84 | "Crown Hts - Utica Av","CROWN HTS-UTICA" 85 | "Sutter Av - Rutland Rd","SUTTER AV-RUTLD" 86 | "Saratoga Av","SARATOGA AV" 87 | "Rockaway Av","ROCKAWAY PARK B" 88 | "Junius St","JUNIUS ST" 89 | "Pennsylvania Av","PENNSYLVANIA AV" 90 | "Van Siclen Av",hasTwo 91 | "New Lots Av","NEW LOTS AV" 92 | "Harlem - 148 St","HARLEM 148 ST" 93 | "Woodlawn","Woodlawn" 94 | "Mosholu Pkwy","MOSHOLU PKWY" 95 | "Bedford Park Blvd - Lehman College","BEDFORD PK BLVD" 96 | "Kingsbridge Rd","KINGSBRIDGE RD" 97 | "Fordham Rd","FORDHAM RD" 98 | "183 St","183 ST" 99 | "Burnside Av","BURNSIDE AV" 100 | "176 St","176 ST" 101 | "Mt Eden Av","MT EDEN AV" 102 | "170 St","170 ST" 103 | "167 St","167 ST" 104 | "161 St - Yankee Stadium","161/YANKEE STAD" 105 | "138 St - Grand Concourse","138/GRAND CONC" 106 | "Bowling Green","BOWLING GREEN" 107 | "59 St","59 ST" 108 | "Grand Central - 42 St","GRD CNTRL-42 ST" 109 | "14 St - Union Sq","14 ST-UNION SQ" 110 | "Brooklyn Bridge - City Hall","BROOKLYN BRIDGE" 111 | "Eastchester - Dyre Av","EASTCHSTER/DYRE" 112 | "Baychester Av","BAYCHESTER AV" 113 | "Morris Park","MORRIS PARK" 114 | "Pelham Bay Park","PELHAM BAY PARK" 115 | "Buhre Av","BUHRE AV" 116 | "Middletown Rd","MIDDLETOWN RD" 117 | "Westchester Sq - E Tremont Av","WESTCHESTER SQ" 118 | "Zerega Av","ZEREGA AV" 119 | "Castle Hill Av","CASTLE HILL AV" 120 | "Parkchester","PARKCHESTER" 121 | "St Lawrence Av","ST LAWRENCE AV" 122 | "Morrison Av- Sound View",none 123 | "Elder Av","ELDER AV" 124 | "Whitlock Av","WHITLOCK AV" 125 | "Hunts Point Av","HUNTS POINT AV" 126 | "Longwood Av","LONGWOOD AV" 127 | "E 149 St","E 149 ST" 128 | "E 143 St - St Mary's St","E 143/ST MARY'S" 129 | "Cypress Av","CYPRESS AV" 130 | "Brook Av",none 131 | "3 Av - 138 St","3 AV 138 ST" 132 | "110 St","110 ST" 133 | "77 St","77 ST" 134 | "68 St - Hunter College","68ST-HUNTER CO" 135 | "51 St","51 ST" 136 | "33 St","33 ST-RAWSON ST" 137 | "Astor Pl","ASTOR PL" 138 | "Bleecker St","BLEECKER ST" 139 | "Spring St","SPRING ST" 140 | "Flushing - Main St","FLUSHING-MAIN" 141 | "Mets - Willets Point","METS-WILLETS PT" 142 | "111 St","111 ST" 143 | "103 St - Corona Plaza","103 ST-CORONA" 144 | "Junction Blvd", 145 | "90 St - Elmhurst Av","ELMHURST AV" 146 | "82 St - Jackson Hts","82 ST-JACKSON H" 147 | "74 St - Broadway","74 ST-BROADWAY" 148 | "69 St","69 ST" 149 | "Woodside - 61 St","61 ST WOODSIDE" 150 | "52 St","52 ST" 151 | "46 St - Bliss St","46 ST" 152 | "40 St - 52 ST St",none 153 | "33 St - Rawson St","33 ST-RAWSON ST" 154 | "Queensboro Plaza","QUEENSBORO PLZ" 155 | "Court Sq","COURT SQ" 156 | "Hunters Point Av","HUNTERS PT AV" 157 | "Vernon Blvd - Jackson Av","VERNON-JACKSON" 158 | "5 Av","5 AVE" 159 | "34 St - Hudson Yds","34 ST-HUDSON YD" 160 | "Inwood - 207 St","INWOOD-207 ST" 161 | "190 St","190 ST" 162 | "175 St","175 ST" 163 | "168 St","168 ST" 164 | "42 St - Port Authority Bus Terminal","42 ST-PORT AUTH" 165 | "W 4 St - Wash Sq","W 4 ST-WASH SQ" 166 | "High St","HIGH ST" 167 | "Jay St - MetroTech","JAY ST-METROTEC" 168 | "Hoyt - Schermerhorn Sts","HOYT-SCHER" 169 | "Utica Av","UTICA AV" 170 | "Broadway Jct","BROADWAY" 171 | "Euclid Av","EUCLID AV" 172 | "Grant Av","GRANT AV" 173 | "80 St","80 ST" 174 | "88 St","88 ST" 175 | "Rockaway Blvd","ROCKAWAY AV" 176 | "104 St","104 ST" 177 | "Ozone Park - Lefferts Blvd","OZONE PK LEFFRT" 178 | "Aqueduct Racetrack","AQUEDUCT RACETR" 179 | "Aqueduct - N Conduit Av","AQUEDUCT N.COND" 180 | "Howard Beach - JFK Airport","HOWARD BCH JFK" 181 | "Broad Channel","BROAD CHANNEL" 182 | "Beach 67 St","BEACH 67 ST" 183 | "Beach 60 St","BEACH 60 ST" 184 | "Beach 44 St","BEACH 44 ST" 185 | "Beach 36 St","BEACH 36 ST" 186 | "Beach 25 St","BEACH 25 ST" 187 | "Far Rockaway - Mott Av","FAR ROCKAWAY" 188 | "81 St - Museum of Natural History","81 ST-MUSEUM" 189 | "Bedford Park Blvd","BEDFORD PK BLVD" 190 | "182-183 Sts","182-183 STS" 191 | "Tremont Av","TREMONT AV" 192 | "174-175 Sts","174-175 STS" 193 | "155 St","155 ST" 194 | "7 Av","7 AV" 195 | "47-50 Sts - Rockefeller Ctr","47-50 STS ROCK" 196 | "42 St - Bryant Pk","42 ST-BRYANT PK" 197 | "34 St - Herald Sq","34 ST-HERALD SQ" 198 | "Broadway-Lafayette St","B'WAY-LAFAYETTE" 199 | "Grand St","GRAND ST" 200 | "Prospect Park","PROSPECT PARK" 201 | "Newkirk Plaza","NEWKIRK AV" 202 | "Kings Hwy","KINGS HWY" 203 | "Sheepshead Bay","SHEEPSHEAD BAY" 204 | "Brighton Beach","BRIGHTON BEACH" 205 | "DeKalb Av","DEKALB AV" 206 | "163 St - Amsterdam Av","163 ST-AMSTERDM" 207 | "Lafayette Av","LAFAYETTE AV" 208 | "Clinton - Washington Avs","CLINTON-WASH AV" 209 | "Kingston - Throop Avs","KINGSTON AV" 210 | "Ralph Av","RALPH AV" 211 | "Liberty Av","LIBERTY AV" 212 | "Shepherd Av","SHEPHERD AV" 213 | "9 Av","9 AV" 214 | "Fort Hamilton Pkwy","FT HAMILTON PKY" 215 | "55 St","55 ST" 216 | "62 St","62 ST" 217 | "71 St","71 ST" 218 | "18 Av","18 AV" 219 | "20 Av","20 AV" 220 | "Bay Pkwy","BAY PKWY" 221 | "25 Av","25 AV" 222 | "Bay 50 St","BAY 50 ST" 223 | "Norwood - 205 St","NORWOOD 205 ST" 224 | "Coney Island - Stillwell Av","CONEY IS-STILLW" 225 | "36 St","36 ST" 226 | "World Trade Center","WORLD TRADE CTR" 227 | "Briarwood","BRIARWOOD" 228 | "Kew Gardens - Union Tpke","KEW GARDENS" 229 | "75 Av","75 AV" 230 | "Lexington Av/53 St","LEXINGTON AV/53" 231 | "5 Av/53 St","5 AV/59 ST" 232 | "Jamaica Center - Parsons/Archer","JAMAICA CENTER" 233 | "Sutphin Blvd - Archer Av - JFK Airport","SUTPHIN-ARCHER" 234 | "Jamaica - Van Wyck","JAMAICA VAN WK" 235 | "Forest Hills - 71 Av","FOREST HILLS 71" 236 | "Jackson Hts - Roosevelt Av",none 237 | "Queens Plaza","QUEENS PLAZA" 238 | "21 St - Queensbridge","21 ST-QNSBRIDGE" 239 | "Roosevelt Island","ROOSEVELT ISLND" 240 | "Lexington Av/63 St","LEXINGTON AV/63" 241 | "57 St","57 ST" 242 | "W 8 St - NY Aquarium","W 8 ST-AQUARIUM" 243 | "Jamaica - 179 St","JAMAICA 179 ST" 244 | "169 St","169 ST" 245 | "Parsons Blvd","PARSONS BLVD" 246 | "Sutphin Blvd","SUTPHIN BLVD" 247 | "2 Av","2 AV" 248 | "Delancey St - Essex St","DELANCEY/ESSEX" 249 | "East Broadway","EAST BROADWAY" 250 | "York St","YORK ST" 251 | "Carroll St","CARROLL ST" 252 | "Smith - 9 Sts","SMITH-9 ST" 253 | "4 Av - 9 St","4 AV-9 ST" 254 | "15 St - Prospect Park","15 ST-PROSPECT" 255 | "Ditmas Av","DITMAS AV" 256 | "Avenue I","Avenue I" 257 | "Avenue N","Avenue N" 258 | "Avenue P","Avenue P" 259 | "Avenue U","Avenue U" 260 | "Avenue X","Avenue X" 261 | "Neptune Av","NEPTUNE AV" 262 | "Court Sq - 23 St","COURT SQ-23 ST" 263 | "21 St","21 ST" 264 | "Greenpoint Av","GREENPOINT AV" 265 | "Nassau Av","NASSAU AV" 266 | "Metropolitan Av","METROPOLITAN AV" 267 | "Broadway","BROADWAY" 268 | "Flushing Av","FLUSHING AV" 269 | "Myrtle - Willoughby Avs","MYRTLE-WILLOUGH" 270 | "Bedford - Nostrand Avs","BEDFORD-NOSTRAN" 271 | "Classon Av","CLASSON AV" 272 | "Beach 90 St","BEACH 90 ST" 273 | "Beach 98 St","BEACH 98 ST" 274 | "Beach 105 St","BEACH 105 ST" 275 | "Rockaway Park - Beach 116 St","ROCKAWAY PARK B" 276 | "121 St","121 ST" 277 | "Woodhaven Blvd","WOODHAVEN BLVD" 278 | "85 St - Forest Pkwy","85 ST-FOREST PK" 279 | "75 St - Elder Ln","75 ST-ELDERTS" 280 | "Cypress Hills","CYPRESS HILLS" 281 | "Crescent St","CRESCENT ST" 282 | "Norwood Av","NORWOOD AV" 283 | "Cleveland St","CLEVELAND ST" 284 | "Alabama Av","ALABAMA AV" 285 | "Chauncey St","CHAUNCEY ST" 286 | "Halsey St","HALSEY ST" 287 | "Gates Av","GATES AV" 288 | "Kosciuszko St","KOSCIUSZKO ST" 289 | "Myrtle Av","MYRTLE AV" 290 | "Lorimer St","LORIMER ST" 291 | "Hewes St","HEWES ST" 292 | "Marcy Av","MARCY AV" 293 | "Delancy St - Essex St","DELANCEY/ESSEX" 294 | "Bowery","BOWERY" 295 | "Broad St","BROAD ST" 296 | "8 Av","8 AV" 297 | "6 Av","6 AV" 298 | "Union Sq - 14 St","14 ST-UNION SQ" 299 | "3 Av",multi 300 | "1 Av",multi 301 | "Bedford Av",none 302 | "Graham Av",none 303 | "Montrose Av", 304 | "Morgan Av",none 305 | "Jefferson St",none 306 | "Myrtle - Wyckoff Avs",none 307 | "Wilson Av",none 308 | "Bushwick Av - Aberdeen St",none 309 | "Atlantic Av",none 310 | "Sutter Av","SUTTER AV-RUTLD" 311 | "Livonia Av",none 312 | "E 105 St",none 313 | "Canarsie - Rockaway Pkwy",none 314 | "67 Av","67 AV" 315 | "63 Dr - Rego Park","63 DR-REGO PARK" 316 | "Grand Av - Newtown","GRAND-NEWTOWN" 317 | "Elmhurst Av","ELMHURST AV" 318 | "65 St","65 ST" 319 | "Northern Blvd","NORTHERN BLVD" 320 | "46 St","46 ST" 321 | "Steinway St","STEINWAY ST" 322 | "Middle Village - Metropolitan Av","METROPOLITAN AV" 323 | "Fresh Pond Rd","FRESH POND RD" 324 | "Forest Av","FOREST AVE" 325 | "Seneca Av","SENECA AVE" 326 | "Knickerbocker Av","KNICKERBOCKER" 327 | "Central Av","CENTRAL AV" 328 | "New Utrecht Av","NEW UTRECHT AV" 329 | "Astoria - Ditmars Blvd","ASTORIA DITMARS" 330 | "Astoria Blvd","ASTORIA BLVD" 331 | "30 Av","30 AV" 332 | "36 Av","36 AV" 333 | "39 Av - Dutch Kills","39 AV" 334 | "Lexington Av/59 St",none 335 | "5 Av/59 St","5 AV/59 ST" 336 | "57 St - 7 Av","57 ST-7 AV" 337 | "49 St","49 ST" 338 | "8 St - NYU","8 ST-NYU" 339 | "Prince St","PRINCE ST" 340 | "City Hall","CITY HALL" 341 | "Whitehall St - South Ferry","WHITEHALL S-FRY" 342 | "Parkside Av","PARKSIDE AV" 343 | "Beverley Rd","BEVERLEY ROAD" 344 | "Cortelyou Rd","CORTELYOU RD" 345 | "Avenue H","AVENUE H" 346 | "Avenue J","AVENUE J" 347 | "Avenue M","AVENUE M" 348 | "Neck Rd","NECK RD" 349 | "Ocean Pkwy","OCEAN PKWY" 350 | "Court St",none 351 | "Union St","UNION ST" 352 | "25 St","25 ST" 353 | "45 St","45 ST" 354 | "53 St","53 ST" 355 | "Bay Ridge Av","BAY RIDGE AV" 356 | "Bay Ridge - 95 St","BAY RIDGE-95 ST" 357 | "Tottenville",none 358 | "Arthur Kill",none 359 | "Richmond Valley",none 360 | "Pleasant Plains",none 361 | "Prince's Bay",none 362 | "Huguenot",none 363 | "Annadale",none 364 | "Eltingville",none 365 | "Great Kills",none 366 | "Bay Terrace",none 367 | "Oakwood Heights",none 368 | "New Dorp",none 369 | "Grant City",none 370 | "Jefferson Av",none 371 | "Dongan Hills",none 372 | "Old Town",none 373 | "Grasmere",none 374 | "Clifton",none 375 | "Stapleton",none 376 | "Tompkinsville",none 377 | "St George",none -------------------------------------------------------------------------------- /scripts/data/cross_walks.csv: -------------------------------------------------------------------------------- 1 | name,id 2 | "Van Cortlandt Park - 242 St",none 3 | "238 St","238 ST" 4 | "231 St","231 ST" 5 | "Marble Hill - 225 St","MARBLE HILL-225" 6 | "215 St","215 ST" 7 | "207 St","207 ST" 8 | "Dyckman St","DYCKMAN ST" 9 | "191 St","191 ST" 10 | "181 St","181 ST" 11 | "168 St - Washington Hts","168 ST" 12 | "157 St","157 ST" 13 | "145 St","145 ST" 14 | "137 St - City College","137 ST CITY COL" 15 | "125 St","125 ST" 16 | "116 St - Columbia University","116 ST-COLUMBIA" 17 | "Cathedral Pkwy (110 St)","CATHEDRAL PKWY" 18 | "103 St","103 ST" 19 | "96 St","96 ST" 20 | "86 St","86 ST" 21 | "79 St","79 ST" 22 | "72 St","72 ST" 23 | "66 St - Lincoln Center","66 ST-LINCOLN" 24 | "59 St - Columbus Circle","59 ST COLUMBUS" 25 | "50 St","50 ST" 26 | "Times Sq - 42 St","TIMES SQ-42 ST" 27 | "34 St - Penn Station","34 ST-PENN STA" 28 | "28 St","28 ST" 29 | "23 St","23 ST" 30 | "18 St","18 ST" 31 | "14 St","14 ST" 32 | "Christopher St - Sheridan Sq","CHRISTOPHER ST" 33 | "Houston St","HOUSTON ST" 34 | "Canal St","CANAL ST" 35 | "Franklin St","FRANKLIN AV" 36 | "Chambers St","CHAMBERS ST" 37 | "Cortlandt St","CORTLANDT ST" 38 | "Rector St","RECTOR ST" 39 | "South Ferry","SOUTH FERRY" 40 | "Wakefield - 241 St","WAKEFIELD/241" 41 | "Nereid Av","NEREID AV" 42 | "233 St","233 ST" 43 | "225 St","225 ST" 44 | "219 St","219 ST" 45 | "Gun Hill Rd","GUN HILL RD" 46 | "Burke Av","BURKE AV" 47 | "Allerton Av","ALLERTON AV" 48 | "Pelham Pkwy","PELHAM PKWY" 49 | "Bronx Park East","BRONX PARK EAST" 50 | "E 180 St","E 180 ST" 51 | "West Farms Sq - E Tremont Av","WEST FARMS SQ" 52 | "174 St","174 ST" 53 | "Freeman St","FREEMAN ST" 54 | "Simpson St","SIMPSON ST" 55 | "Intervale Av","INTERVALE AV" 56 | "Prospect Av","PROSPECT AV" 57 | "Jackson Av","JACKSON AV" 58 | "3 Av - 149 St","3 AV-149 ST" 59 | "149 St - Grand Concourse","149/GRAND CONC" 60 | "135 St","135 ST" 61 | "116 St","116 ST" 62 | "Central Park North (110 St)","CENTRAL PK N110" 63 | "Park Pl","PARK PLACE" 64 | "Fulton St","FULTON ST" 65 | "Wall St","WALL ST" 66 | "Clark St","CLARK ST" 67 | "Borough Hall","BOROUGH HALL" 68 | "Hoyt St","HOYT ST" 69 | "Nevins St","NEVINS ST" 70 | "Atlantic Av - Barclays Ctr","ATL AV-BARCLAY" 71 | "Bergen St","BERGEN ST" 72 | "Grand Army Plaza","GRAND ARMY PLAZ" 73 | "Eastern Pkwy - Brooklyn Museum",none 74 | "Franklin Av","FRANKLIN AV" 75 | "President St","PRESIDENT ST" 76 | "Sterling St","STERLING ST" 77 | "Winthrop St","WINTHROP ST" 78 | "Church Av","CHURCH AV" 79 | "Beverly Rd","BEVERLY RD" 80 | "Newkirk Av","NEWKIRK AV" 81 | "Flatbush Av - Brooklyn College","FLATBUSH AV-B.C" 82 | "Nostrand Av","NOSTRAND AV" 83 | "Kingston Av","KINGSTON AV" 84 | "Crown Hts - Utica Av","CROWN HTS-UTICA" 85 | "Sutter Av - Rutland Rd","SUTTER AV-RUTLD" 86 | "Saratoga Av","SARATOGA AV" 87 | "Rockaway Av","ROCKAWAY PARK B" 88 | "Junius St","JUNIUS ST" 89 | "Pennsylvania Av","PENNSYLVANIA AV" 90 | "Van Siclen Av",hasTwo 91 | "New Lots Av","NEW LOTS AV" 92 | "Harlem - 148 St","HARLEM 148 ST" 93 | "Woodlawn","Woodlawn" 94 | "Mosholu Pkwy","MOSHOLU PKWY" 95 | "Bedford Park Blvd - Lehman College","BEDFORD PK BLVD" 96 | "Kingsbridge Rd","KINGSBRIDGE RD" 97 | "Fordham Rd","FORDHAM RD" 98 | "183 St","183 ST" 99 | "Burnside Av","BURNSIDE AV" 100 | "176 St","176 ST" 101 | "Mt Eden Av","MT EDEN AV" 102 | "170 St","170 ST" 103 | "167 St","167 ST" 104 | "161 St - Yankee Stadium","161/YANKEE STAD" 105 | "138 St - Grand Concourse","138/GRAND CONC" 106 | "Bowling Green","BOWLING GREEN" 107 | "59 St","59 ST" 108 | "Grand Central - 42 St","GRD CNTRL-42 ST" 109 | "14 St - Union Sq","14 ST-UNION SQ" 110 | "Brooklyn Bridge - City Hall","BROOKLYN BRIDGE" 111 | "Eastchester - Dyre Av","EASTCHSTER/DYRE" 112 | "Baychester Av","BAYCHESTER AV" 113 | "Morris Park","MORRIS PARK" 114 | "Pelham Bay Park","PELHAM BAY PARK" 115 | "Buhre Av","BUHRE AV" 116 | "Middletown Rd","MIDDLETOWN RD" 117 | "Westchester Sq - E Tremont Av","WESTCHESTER SQ" 118 | "Zerega Av","ZEREGA AV" 119 | "Castle Hill Av","CASTLE HILL AV" 120 | "Parkchester","PARKCHESTER" 121 | "St Lawrence Av","ST LAWRENCE AV" 122 | "Morrison Av- Sound View",none 123 | "Elder Av","ELDER AV" 124 | "Whitlock Av","WHITLOCK AV" 125 | "Hunts Point Av","HUNTS POINT AV" 126 | "Longwood Av","LONGWOOD AV" 127 | "E 149 St","E 149 ST" 128 | "E 143 St - St Mary's St","E 143/ST MARY'S" 129 | "Cypress Av","CYPRESS AV" 130 | "Brook Av",none 131 | "3 Av - 138 St","3 AV 138 ST" 132 | "110 St","110 ST" 133 | "77 St","77 ST" 134 | "68 St - Hunter College","68ST-HUNTER CO" 135 | "51 St","51 ST" 136 | "33 St","33 ST-RAWSON ST" 137 | "Astor Pl","ASTOR PL" 138 | "Bleecker St","BLEECKER ST" 139 | "Spring St","SPRING ST" 140 | "Flushing - Main St","FLUSHING-MAIN" 141 | "Mets - Willets Point","METS-WILLETS PT" 142 | "111 St","111 ST" 143 | "103 St - Corona Plaza","103 ST-CORONA" 144 | "Junction Blvd", 145 | "90 St - Elmhurst Av","ELMHURST AV" 146 | "82 St - Jackson Hts","82 ST-JACKSON H" 147 | "74 St - Broadway","74 ST-BROADWAY" 148 | "69 St","69 ST" 149 | "Woodside - 61 St","61 ST WOODSIDE" 150 | "52 St","52 ST" 151 | "46 St - Bliss St","46 ST" 152 | "40 St - 52 ST St",none 153 | "33 St - Rawson St","33 ST-RAWSON ST" 154 | "Queensboro Plaza","QUEENSBORO PLZ" 155 | "Court Sq","COURT SQ" 156 | "Hunters Point Av","HUNTERS PT AV" 157 | "Vernon Blvd - Jackson Av","VERNON-JACKSON" 158 | "5 Av","5 AVE" 159 | "34 St - Hudson Yds","34 ST-HUDSON YD" 160 | "Inwood - 207 St","INWOOD-207 ST" 161 | "190 St","190 ST" 162 | "175 St","175 ST" 163 | "168 St","168 ST" 164 | "42 St - Port Authority Bus Terminal","42 ST-PORT AUTH" 165 | "W 4 St - Wash Sq","W 4 ST-WASH SQ" 166 | "High St","HIGH ST" 167 | "Jay St - MetroTech","JAY ST-METROTEC" 168 | "Hoyt - Schermerhorn Sts","HOYT-SCHER" 169 | "Utica Av","UTICA AV" 170 | "Broadway Jct","BROADWAY" 171 | "Euclid Av","EUCLID AV" 172 | "Grant Av","GRANT AV" 173 | "80 St","80 ST" 174 | "88 St","88 ST" 175 | "Rockaway Blvd","ROCKAWAY AV" 176 | "104 St","104 ST" 177 | "Ozone Park - Lefferts Blvd","OZONE PK LEFFRT" 178 | "Aqueduct Racetrack","AQUEDUCT RACETR" 179 | "Aqueduct - N Conduit Av","AQUEDUCT N.COND" 180 | "Howard Beach - JFK Airport","HOWARD BCH JFK" 181 | "Broad Channel","BROAD CHANNEL" 182 | "Beach 67 St","BEACH 67 ST" 183 | "Beach 60 St","BEACH 60 ST" 184 | "Beach 44 St","BEACH 44 ST" 185 | "Beach 36 St","BEACH 36 ST" 186 | "Beach 25 St","BEACH 25 ST" 187 | "Far Rockaway - Mott Av","FAR ROCKAWAY" 188 | "81 St - Museum of Natural History","81 ST-MUSEUM" 189 | "Bedford Park Blvd","BEDFORD PK BLVD" 190 | "182-183 Sts","182-183 STS" 191 | "Tremont Av","TREMONT AV" 192 | "174-175 Sts","174-175 STS" 193 | "155 St","155 ST" 194 | "7 Av","7 AV" 195 | "47-50 Sts - Rockefeller Ctr","47-50 STS ROCK" 196 | "42 St - Bryant Pk","42 ST-BRYANT PK" 197 | "34 St - Herald Sq","34 ST-HERALD SQ" 198 | "Broadway-Lafayette St","B'WAY-LAFAYETTE" 199 | "Grand St","GRAND ST" 200 | "Prospect Park","PROSPECT PARK" 201 | "Newkirk Plaza","NEWKIRK AV" 202 | "Kings Hwy","KINGS HWY" 203 | "Sheepshead Bay","SHEEPSHEAD BAY" 204 | "Brighton Beach","BRIGHTON BEACH" 205 | "DeKalb Av","DEKALB AV" 206 | "163 St - Amsterdam Av","163 ST-AMSTERDM" 207 | "Lafayette Av","LAFAYETTE AV" 208 | "Clinton - Washington Avs","CLINTON-WASH AV" 209 | "Kingston - Throop Avs","KINGSTON AV" 210 | "Ralph Av","RALPH AV" 211 | "Liberty Av","LIBERTY AV" 212 | "Shepherd Av","SHEPHERD AV" 213 | "9 Av","9 AV" 214 | "Fort Hamilton Pkwy","FT HAMILTON PKY" 215 | "55 St","55 ST" 216 | "62 St","62 ST" 217 | "71 St","71 ST" 218 | "18 Av","18 AV" 219 | "20 Av","20 AV" 220 | "Bay Pkwy","BAY PKWY" 221 | "25 Av","25 AV" 222 | "Bay 50 St","BAY 50 ST" 223 | "Norwood - 205 St","NORWOOD 205 ST" 224 | "Coney Island - Stillwell Av","CONEY IS-STILLW" 225 | "36 St","36 ST" 226 | "World Trade Center","WORLD TRADE CTR" 227 | "Briarwood","BRIARWOOD" 228 | "Kew Gardens - Union Tpke","KEW GARDENS" 229 | "75 Av","75 AV" 230 | "Lexington Av/53 St","LEXINGTON AV/53" 231 | "5 Av/53 St","5 AV/59 ST" 232 | "Jamaica Center - Parsons/Archer","JAMAICA CENTER" 233 | "Sutphin Blvd - Archer Av - JFK Airport","SUTPHIN-ARCHER" 234 | "Jamaica - Van Wyck","JAMAICA VAN WK" 235 | "Forest Hills - 71 Av","FOREST HILLS 71" 236 | "Jackson Hts - Roosevelt Av",none 237 | "Queens Plaza","QUEENS PLAZA" 238 | "21 St - Queensbridge","21 ST-QNSBRIDGE" 239 | "Roosevelt Island","ROOSEVELT ISLND" 240 | "Lexington Av/63 St","LEXINGTON AV/63" 241 | "57 St","57 ST" 242 | "W 8 St - NY Aquarium","W 8 ST-AQUARIUM" 243 | "Jamaica - 179 St","JAMAICA 179 ST" 244 | "169 St","169 ST" 245 | "Parsons Blvd","PARSONS BLVD" 246 | "Sutphin Blvd","SUTPHIN BLVD" 247 | "2 Av","2 AV" 248 | "Delancey St - Essex St","DELANCEY/ESSEX" 249 | "East Broadway","EAST BROADWAY" 250 | "York St","YORK ST" 251 | "Carroll St","CARROLL ST" 252 | "Smith - 9 Sts","SMITH-9 ST" 253 | "4 Av - 9 St","4 AV-9 ST" 254 | "15 St - Prospect Park","15 ST-PROSPECT" 255 | "Ditmas Av","DITMAS AV" 256 | "Avenue I","Avenue I" 257 | "Avenue N","Avenue N" 258 | "Avenue P","Avenue P" 259 | "Avenue U","Avenue U" 260 | "Avenue X","Avenue X" 261 | "Neptune Av","NEPTUNE AV" 262 | "Court Sq - 23 St","COURT SQ-23 ST" 263 | "21 St","21 ST" 264 | "Greenpoint Av","GREENPOINT AV" 265 | "Nassau Av","NASSAU AV" 266 | "Metropolitan Av","METROPOLITAN AV" 267 | "Broadway","BROADWAY" 268 | "Flushing Av","FLUSHING AV" 269 | "Myrtle - Willoughby Avs","MYRTLE-WILLOUGH" 270 | "Bedford - Nostrand Avs","BEDFORD-NOSTRAN" 271 | "Classon Av","CLASSON AV" 272 | "Beach 90 St","BEACH 90 ST" 273 | "Beach 98 St","BEACH 98 ST" 274 | "Beach 105 St","BEACH 105 ST" 275 | "Rockaway Park - Beach 116 St","ROCKAWAY PARK B" 276 | "121 St","121 ST" 277 | "Woodhaven Blvd","WOODHAVEN BLVD" 278 | "85 St - Forest Pkwy","85 ST-FOREST PK" 279 | "75 St - Elder Ln","75 ST-ELDERTS" 280 | "Cypress Hills","CYPRESS HILLS" 281 | "Crescent St","CRESCENT ST" 282 | "Norwood Av","NORWOOD AV" 283 | "Cleveland St","CLEVELAND ST" 284 | "Alabama Av","ALABAMA AV" 285 | "Chauncey St","CHAUNCEY ST" 286 | "Halsey St","HALSEY ST" 287 | "Gates Av","GATES AV" 288 | "Kosciuszko St","KOSCIUSZKO ST" 289 | "Myrtle Av","MYRTLE AV" 290 | "Lorimer St","LORIMER ST" 291 | "Hewes St","HEWES ST" 292 | "Marcy Av","MARCY AV" 293 | "Delancy St - Essex St","DELANCEY/ESSEX" 294 | "Bowery","BOWERY" 295 | "Broad St","BROAD ST" 296 | "8 Av","8 AV" 297 | "6 Av","6 AV" 298 | "Union Sq - 14 St","14 ST-UNION SQ" 299 | "3 Av",multi 300 | "1 Av",multi 301 | "Bedford Av",none 302 | "Graham Av",none 303 | "Montrose Av", 304 | "Morgan Av",none 305 | "Jefferson St",none 306 | "Myrtle - Wyckoff Avs",none 307 | "Wilson Av",none 308 | "Bushwick Av - Aberdeen St",none 309 | "Atlantic Av",none 310 | "Sutter Av","SUTTER AV-RUTLD" 311 | "Livonia Av",none 312 | "E 105 St",none 313 | "Canarsie - Rockaway Pkwy",none 314 | "67 Av","67 AV" 315 | "63 Dr - Rego Park","63 DR-REGO PARK" 316 | "Grand Av - Newtown","GRAND-NEWTOWN" 317 | "Elmhurst Av","ELMHURST AV" 318 | "65 St","65 ST" 319 | "Northern Blvd","NORTHERN BLVD" 320 | "46 St","46 ST" 321 | "Steinway St","STEINWAY ST" 322 | "Middle Village - Metropolitan Av","METROPOLITAN AV" 323 | "Fresh Pond Rd","FRESH POND RD" 324 | "Forest Av","FOREST AVE" 325 | "Seneca Av","SENECA AVE" 326 | "Knickerbocker Av","KNICKERBOCKER" 327 | "Central Av","CENTRAL AV" 328 | "New Utrecht Av","NEW UTRECHT AV" 329 | "Astoria - Ditmars Blvd","ASTORIA DITMARS" 330 | "Astoria Blvd","ASTORIA BLVD" 331 | "30 Av","30 AV" 332 | "36 Av","36 AV" 333 | "39 Av - Dutch Kills","39 AV" 334 | "Lexington Av/59 St",none 335 | "5 Av/59 St","5 AV/59 ST" 336 | "57 St - 7 Av","57 ST-7 AV" 337 | "49 St","49 ST" 338 | "8 St - NYU","8 ST-NYU" 339 | "Prince St","PRINCE ST" 340 | "City Hall","CITY HALL" 341 | "Whitehall St - South Ferry","WHITEHALL S-FRY" 342 | "Parkside Av","PARKSIDE AV" 343 | "Beverley Rd","BEVERLEY ROAD" 344 | "Cortelyou Rd","CORTELYOU RD" 345 | "Avenue H","AVENUE H" 346 | "Avenue J","AVENUE J" 347 | "Avenue M","AVENUE M" 348 | "Neck Rd","NECK RD" 349 | "Ocean Pkwy","OCEAN PKWY" 350 | "Court St",none 351 | "Union St","UNION ST" 352 | "25 St","25 ST" 353 | "45 St","45 ST" 354 | "53 St","53 ST" 355 | "Bay Ridge Av","BAY RIDGE AV" 356 | "Bay Ridge - 95 St","BAY RIDGE-95 ST" 357 | "Tottenville",none 358 | "Arthur Kill",none 359 | "Richmond Valley",none 360 | "Pleasant Plains",none 361 | "Prince's Bay",none 362 | "Huguenot",none 363 | "Annadale",none 364 | "Eltingville",none 365 | "Great Kills",none 366 | "Bay Terrace",none 367 | "Oakwood Heights",none 368 | "New Dorp",none 369 | "Grant City",none 370 | "Jefferson Av",none 371 | "Dongan Hills",none 372 | "Old Town",none 373 | "Grasmere",none 374 | "Clifton",none 375 | "Stapleton",none 376 | "Tompkinsville",none 377 | "St George",none -------------------------------------------------------------------------------- /scripts/data/google_transit/agency.txt: -------------------------------------------------------------------------------- 1 | agency_id,agency_name,agency_url,agency_timezone,agency_lang,agency_phone 2 | MTA NYCT,MTA New York City Transit, http://www.mta.info,America/New_York,en,718-330-1234 3 | -------------------------------------------------------------------------------- /scripts/data/google_transit/calendar.txt: -------------------------------------------------------------------------------- 1 | service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date 2 | AFA19GEN-1087-Weekday-00,1,1,1,1,1,0,0,20200102,20200501 3 | AFA19GEN-2097-Weekday-00,1,1,1,1,1,0,0,20200102,20200501 4 | AFA19GEN-3087-Weekday-00,1,1,1,1,1,0,0,20200102,20200501 5 | AFA19GEN-4098-Weekday-00,1,1,1,1,1,0,0,20200102,20200501 6 | AFA19GEN-5107-Weekday-00,1,1,1,1,1,0,0,20200102,20200501 7 | AFA19GEN-6086-Weekday-00,1,1,1,1,1,0,0,20200102,20200501 8 | AFA19GEN-7060-Weekday-00,1,1,1,1,1,0,0,20200102,20200501 9 | BFA19GEN-A083-Weekday-00,1,1,1,1,1,0,0,20200102,20200501 10 | BFA19GEN-B081-Weekday-00,1,1,1,1,1,0,0,20200102,20200501 11 | BFA19GEN-C051-Weekday-00,1,1,1,1,1,0,0,20200102,20200501 12 | BFA19GEN-D077-Weekday-00,1,1,1,1,1,0,0,20200102,20200501 13 | BFA19GEN-E073-Weekday-00,1,1,1,1,1,0,0,20200102,20200501 14 | BFA19GEN-F074-Weekday-00,1,1,1,1,1,0,0,20200102,20200501 15 | BFA19GEN-FS011-Weekday-00,1,1,1,1,1,0,0,20200102,20200501 16 | BFA19GEN-G051-Weekday-00,1,1,1,1,1,0,0,20200102,20200501 17 | BFA19GEN-G051-Weekday-00-0111100,0,1,1,1,1,0,0,20200102,20200501 18 | AFA19GEN-GS019-Weekday-00,1,1,1,1,1,0,0,20200102,20200501 19 | BFA19GEN-H039-Weekday-00,1,1,1,1,1,0,0,20200102,20200501 20 | BFA19GEN-H039-Weekday-00-0111100,0,1,1,1,1,0,0,20200102,20200501 21 | BFA19GEN-J052-Weekday-00,1,1,1,1,1,0,0,20200102,20200501 22 | BFA19SUPP-L047-Weekday-99,1,1,1,1,1,0,0,20200102,20200501 23 | BFA19GEN-M088-Weekday-00,1,1,1,1,1,0,0,20200102,20200501 24 | BFA19GEN-M088-Weekday-00-1111000,1,1,1,1,0,0,0,20200102,20200501 25 | BFA19GEN-N093-Weekday-00,1,1,1,1,1,0,0,20200102,20200501 26 | BFA19GEN-Q062-Weekday-00,1,1,1,1,1,0,0,20200102,20200501 27 | BFA19GEN-R089-Weekday-00,1,1,1,1,1,0,0,20200102,20200501 28 | SIR-FA2017-SI017-Weekday-08,1,1,1,1,1,0,0,20200102,20200501 29 | AFA19GEN-1038-Saturday-00,0,0,0,0,0,1,0,20200104,20200502 30 | AFA19GEN-2042-Saturday-00,0,0,0,0,0,1,0,20200104,20200502 31 | AFA19GEN-3039-Saturday-00,0,0,0,0,0,1,0,20200104,20200502 32 | AFA19GEN-4043-Saturday-00,0,0,0,0,0,1,0,20200104,20200502 33 | AFA19GEN-5043-Saturday-00,0,0,0,0,0,1,0,20200104,20200502 34 | AFA19GEN-6033-Saturday-00,0,0,0,0,0,1,0,20200104,20200502 35 | AFA19GEN-7023-Saturday-00,0,0,0,0,0,1,0,20200104,20200502 36 | BFA19GEN-A045-Saturday-00,0,0,0,0,0,1,0,20200104,20200502 37 | BFA19GEN-C025-Saturday-00,0,0,0,0,0,1,0,20200104,20200502 38 | BFA19GEN-D037-Saturday-00,0,0,0,0,0,1,0,20200104,20200502 39 | BFA19GEN-E041-Saturday-00,0,0,0,0,0,1,0,20200104,20200502 40 | BFA19GEN-F044-Saturday-00,0,0,0,0,0,1,0,20200104,20200502 41 | BFA19GEN-FS011-Saturday-00,0,0,0,0,0,1,0,20200104,20200502 42 | BFA19GEN-G035-Saturday-00,0,0,0,0,0,1,0,20200104,20200502 43 | AFA19GEN-GS010-Saturday-00,0,0,0,0,0,1,0,20200104,20200502 44 | BFA19GEN-H025-Saturday-00,0,0,0,0,0,1,0,20200104,20200502 45 | BFA19GEN-J032-Saturday-00,0,0,0,0,0,1,0,20200104,20200502 46 | BFA19SUPP-L024-Saturday-99,0,0,0,0,0,1,0,20200104,20200502 47 | BFA19GEN-M027-Saturday-00,0,0,0,0,0,1,0,20200104,20200502 48 | BFA19GEN-N055-Saturday-00,0,0,0,0,0,1,0,20200104,20200502 49 | BFA19GEN-Q021-Saturday-00,0,0,0,0,0,1,0,20200104,20200502 50 | BFA19GEN-R051-Saturday-00,0,0,0,0,0,1,0,20200104,20200502 51 | SIR-FA2017-SI017-Saturday-00,0,0,0,0,0,1,0,20200104,20200502 52 | AFA19GEN-1037-Sunday-00,0,0,0,0,0,0,1,20200105,20200426 53 | AFA19GEN-2048-Sunday-00,0,0,0,0,0,0,1,20200105,20200426 54 | AFA19GEN-3041-Sunday-00,0,0,0,0,0,0,1,20200105,20200426 55 | AFA19GEN-4049-Sunday-00,0,0,0,0,0,0,1,20200105,20200426 56 | AFA19GEN-5048-Sunday-00,0,0,0,0,0,0,1,20200105,20200426 57 | AFA19GEN-6030-Sunday-00,0,0,0,0,0,0,1,20200105,20200426 58 | AFA19GEN-7025-Sunday-00,0,0,0,0,0,0,1,20200105,20200426 59 | BFA19GEN-A052-Sunday-00,0,0,0,0,0,0,1,20200105,20200426 60 | BFA19GEN-C027-Sunday-00,0,0,0,0,0,0,1,20200105,20200426 61 | BFA19GEN-D040-Sunday-00,0,0,0,0,0,0,1,20200105,20200426 62 | BFA19GEN-E046-Sunday-00,0,0,0,0,0,0,1,20200105,20200426 63 | BFA19GEN-F048-Sunday-00,0,0,0,0,0,0,1,20200105,20200426 64 | BFA19GEN-FS012-Sunday-00,0,0,0,0,0,0,1,20200105,20200426 65 | BFA19GEN-G036-Sunday-00,0,0,0,0,0,0,1,20200105,20200426 66 | AFA19GEN-GS010-Sunday-00,0,0,0,0,0,0,1,20200105,20200426 67 | BFA19GEN-H024-Sunday-00,0,0,0,0,0,0,1,20200105,20200426 68 | BFA19GEN-J032-Sunday-00,0,0,0,0,0,0,1,20200105,20200426 69 | BFA19SUPP-L024-Sunday-99,0,0,0,0,0,0,1,20200105,20200426 70 | BFA19GEN-M026-Sunday-00,0,0,0,0,0,0,1,20200105,20200426 71 | BFA19GEN-N058-Sunday-00,0,0,0,0,0,0,1,20200105,20200426 72 | BFA19GEN-Q023-Sunday-00,0,0,0,0,0,0,1,20200105,20200426 73 | BFA19GEN-R050-Sunday-00,0,0,0,0,0,0,1,20200105,20200426 74 | SIR-FA2017-SI017-Sunday-00,0,0,0,0,0,0,1,20200105,20200426 75 | -------------------------------------------------------------------------------- /scripts/data/google_transit/calendar_dates.txt: -------------------------------------------------------------------------------- 1 | service_id,date,exception_type 2 | AFA19GEN-1087-Weekday-00,20200217,2 3 | AFA19GEN-2097-Weekday-00,20200217,2 4 | AFA19GEN-3087-Weekday-00,20200217,2 5 | AFA19GEN-4098-Weekday-00,20200217,2 6 | AFA19GEN-5107-Weekday-00,20200217,2 7 | AFA19GEN-6086-Weekday-00,20200217,2 8 | AFA19GEN-7060-Weekday-00,20200217,2 9 | BFA19GEN-A083-Weekday-00,20200217,2 10 | BFA19GEN-B081-Weekday-00,20200217,2 11 | BFA19GEN-C051-Weekday-00,20200217,2 12 | BFA19GEN-D077-Weekday-00,20200217,2 13 | BFA19GEN-E073-Weekday-00,20200217,2 14 | BFA19GEN-F074-Weekday-00,20200217,2 15 | BFA19GEN-FS011-Weekday-00,20200217,2 16 | BFA19GEN-G051-Weekday-00,20200217,2 17 | BFA19GEN-G051-Weekday-00-0111100,20200217,2 18 | AFA19GEN-GS019-Weekday-00,20200217,2 19 | BFA19GEN-H039-Weekday-00,20200217,2 20 | BFA19GEN-H039-Weekday-00-0111100,20200217,2 21 | BFA19GEN-J052-Weekday-00,20200217,2 22 | BFA19SUPP-L047-Weekday-99,20200217,2 23 | BFA19GEN-M088-Weekday-00,20200217,2 24 | BFA19GEN-M088-Weekday-00-1111000,20200217,2 25 | BFA19GEN-N093-Weekday-00,20200217,2 26 | BFA19GEN-Q062-Weekday-00,20200217,2 27 | BFA19GEN-R089-Weekday-00,20200217,2 28 | SIR-FA2017-SI017-Weekday-08,20200217,2 29 | AFA19GEN-1038-Saturday-00,20200217,1 30 | AFA19GEN-2042-Saturday-00,20200217,1 31 | AFA19GEN-3039-Saturday-00,20200217,1 32 | AFA19GEN-4043-Saturday-00,20200217,1 33 | AFA19GEN-5043-Saturday-00,20200217,1 34 | AFA19GEN-6033-Saturday-00,20200217,1 35 | AFA19GEN-7023-Saturday-00,20200217,1 36 | BFA19GEN-A045-Saturday-00,20200217,1 37 | BFA19GEN-C025-Saturday-00,20200217,1 38 | BFA19GEN-D037-Saturday-00,20200217,1 39 | BFA19GEN-E041-Saturday-00,20200217,1 40 | BFA19GEN-F044-Saturday-00,20200217,1 41 | BFA19GEN-FS011-Saturday-00,20200217,1 42 | BFA19GEN-G035-Saturday-00,20200217,1 43 | AFA19GEN-GS010-Saturday-00,20200217,1 44 | BFA19GEN-H025-Saturday-00,20200217,1 45 | BFA19GEN-J032-Saturday-00,20200217,1 46 | BFA19SUPP-L024-Saturday-99,20200217,1 47 | BFA19GEN-M027-Saturday-00,20200217,1 48 | BFA19GEN-N055-Saturday-00,20200217,1 49 | BFA19GEN-Q021-Saturday-00,20200217,1 50 | BFA19GEN-R051-Saturday-00,20200217,1 51 | SIR-FA2017-SI017-Saturday-00,20200217,1 52 | -------------------------------------------------------------------------------- /scripts/data/google_transit/routes.txt: -------------------------------------------------------------------------------- 1 | route_id,agency_id,route_short_name,route_long_name,route_desc,route_type,route_url,route_color,route_text_color 2 | 1,MTA NYCT,1,Broadway - 7 Avenue Local,"Trains operate between 242 St in the Bronx and South Ferry in Manhattan, at all times",1,http://web.mta.info/nyct/service/pdf/t1cur.pdf,EE352E, 3 | 2,MTA NYCT,2,7 Avenue Express,"Trains operate between Wakefield-241 St, Bronx, and Flatbush Av-Brooklyn College, Brooklyn, at all times. Trains operate local in Bronx and Brooklyn. Trains operate express in Manhattan except late night when it operates local.",1,http://web.mta.info/nyct/service/pdf/t2cur.pdf,EE352E, 4 | 3,MTA NYCT,3,7 Avenue Express,"Trains operate between 148 St, 7 Av, Manhattan, and New Lots Av, Brooklyn, at all times except late nights. During late nights, trains operate only in Manhattan between 148 St, 7 Av and Times Square-42 St.",1,http://web.mta.info/nyct/service/pdf/t3cur.pdf,EE352E, 5 | 4,MTA NYCT,4,Lexington Avenue Express,"Trains operate daily between Woodlawn/Jerome Av, Bronx, and Utica Av/Eastern Pkwy, Brooklyn, running express in Manhattan and Brooklyn. During late night and early morning hours, trains run local in Manhattan and Brooklyn, and extend beyond Utica Av to New Lots/Livonia Avs, Brooklyn.",1,http://web.mta.info/nyct/service/pdf/t4cur.pdf,00933C, 6 | 5,MTA NYCT,5,Lexington Avenue Express,"Weekdays daytime, most trains operate between either Dyre Av or 238 St-Nereid Av, Bronx, and Flatbush Av-Brooklyn College, Brooklyn. At all other times except during late nights, trains operate between Dyre Av, Bronx, and Bowling Green, Manhattan. During late nights trains operate only in the Bronx between Dyre Av and E 180 St/MorrisPark Av. Customers who ride during late night hours can transfer to 2 service at the E 180 St Station. At all times, trains operate express in Manhattan and Brooklyn. Weekdays, trains in the Bronx operate express from E 180 St to 149 St-3 Av during morning rush hours (from about 6 AM to 9 AM), and from 149 St-3 Av to E 180 St during the evening rush hours (from about 4 PM to 7 PM).",1,http://web.mta.info/nyct/service/pdf/t5cur.pdf,00933C, 7 | 5X,MTA NYCT,5X,Lexington Avenue Express,"Weekdays daytime, most trains operate between either Dyre Av or 238 St-Nereid Av, Bronx, and Flatbush Av-Brooklyn College, Brooklyn. At all other times except during late nights, trains operate between Dyre Av, Bronx, and Bowling Green, Manhattan. During late nights trains operate only in the Bronx between Dyre Av and E 180 St/MorrisPark Av. Customers who ride during late night hours can transfer to 2 service at the E 180 St Station. At all times, trains operate express in Manhattan and Brooklyn. Weekdays, trains in the Bronx operate express from E 180 St to 149 St-3 Av during morning rush hours (from about 6 AM to 9 AM), and from 149 St-3 Av to E 180 St during the evening rush hours (from about 4 PM to 7 PM).",1,http://web.mta.info/nyct/service/pdf/t5cur.pdf,00933C, 8 | 6,MTA NYCT,6,Lexington Avenue Local,"Local trains operate between Pelham Bay Park/Bruckner Expwy, Bronx, and Brooklyn Bridge/City Hall, Manhattan, at all times.",1,http://web.mta.info/nyct/service/pdf/t6cur.pdf,00933C, 9 | 6X,MTA NYCT,6X,Pelham Bay Park Express,"Express trains operate between Pelham Bay Park/Bruckner Expwy, Bronx, and Brooklyn Bridge/City Hall, Manhattan, weekday mornings express in the Bronx toward Manhattan. Weekday afternoons and evenings, these trains operate express in the Bronx toward Pelham Bay Park.",1,http://web.mta.info/nyct/service/pdf/t6cur.pdf,00A65C, 10 | 7,MTA NYCT,7,Flushing Local,"Trains operate between Main St-Flushing, Queens, and 34th-Hudson Yards, Manhattan, at all times. ",1,http://web.mta.info/nyct/service/pdf/t7cur.pdf,B933AD, 11 | 7X,MTA NYCT,7X,Flushing Express,"Trains operate between Main St-Flushing, Queens, and 34th St-Hudson Yards, Manhattan, weekday mornings toward Manhattan. Weekday afternoons and evenings, these trains operate express to Queens.",1,http://web.mta.info/nyct/service/pdf/t7cur.pdf,B933AD, 12 | GS,MTA NYCT,S,42 St Shuttle,"Operates in Manhattan between Grand Central and Times Square. The shuttle provides a free transfer between 4, 5, 6, and 7 service at Grand Central-42 St and A, C, E, N, Q, R, W, 1, 2, 3, and 7 service at Times Square-42 St. The shuttle runs at all times except during late nights, from about 12 midnight to 6 AM.",1,http://web.mta.info/nyct/service/pdf/t0cur.pdf,6D6E71, 13 | A,MTA NYCT,A,8 Avenue Express,"Trains operate between Inwood-207 St, Manhattan and Far Rockaway-Mott Avenue, Queens at all times. Also from about 6 AM until about midnight, additional trains operate between Inwood-207 St and Lefferts Boulevard (trains typically alternate between Lefferts Blvd and Far Rockaway). During weekday morning rush hours, special trains operate from Rockaway Park-Beach 116 St, Queens, toward Manhattan. These trains make local stops between Rockaway Park and Broad Channel. Similarly, in the evening rush hour special trains leave Manhattan operating toward Rockaway Park-Beach 116 St, Queens.",1,http://web.mta.info/nyct/service/pdf/tacur.pdf,2850AD,FFFFFF 14 | B,MTA NYCT,B,6 Avenue Express,"Trains operate, weekdays only, between 145 St, Manhattan, and Brighton Beach, Brooklyn at all times except late nights. The route extends to Bedford Park Blvd, Bronx, during rush hours.",1,http://web.mta.info/nyct/service/pdf/tbcur.pdf,FF6319, 15 | C,MTA NYCT,C,8 Avenue Local,"Trains operate between 168 St, Manhattan, and Euclid Av, Brooklyn, daily from about 6 AM to 11 PM.",1,http://web.mta.info/nyct/service/pdf/tccur.pdf,2850AD,FFFFFF 16 | D,MTA NYCT,D,6 Avenue Express,"Trains operate, at all times, from 205 Street, Bronx, to Stillwell Avenue, Brooklyn via Central Park West and 6th Avenue in Manhattan, and via the Manhattan Bridge to and from Brooklyn. When in Brooklyn trains operate via 4th Avenue then through Bensonhurst to Coney Island. Trains typically operate local in the Bronx, express in Manhattan, and local in Brooklyn. But please note that Bronx rush hour trains operate express (peak direction ONLY), and Brooklyn trains operate express along the 4th Avenue segment (all times except late nights).",1,http://web.mta.info/nyct/service/pdf/tdcur.pdf,FF6319, 17 | E,MTA NYCT,E,8 Avenue Local,"Trains operate between Jamaica Center (Parsons/Archer), Queens, and World Trade Center, Manhattan, at all times. E trains operate express in Queens at all times except late nights when they operate local.",1,http://web.mta.info/nyct/service/pdf/tecur.pdf,2850AD,FFFFFF 18 | F,MTA NYCT,F,Queens Blvd Express/ 6 Av Local,"Trains operate at all times between Jamaica-179 St, Queens, and Stillwell Av, Brooklyn via the 63 St Connector (serving 21 St-Queensbridge, Roosevelt Island, Lexington Av-63 St, and 57 St-6 Av). F trains operate local in Manhattan and express in Queens at all times.",1,http://web.mta.info/nyct/service/pdf/tfcur.pdf,FF6319, 19 | FX,MTA NYCT,FX,Brooklyn F Express,"Trains operate rush hour only between Jamaica-179 St, Queens, and Stillwell Av, Brooklyn, weekday mornings toward Manhattan, weekday afternoons toward Coney Island. These trains operate express in Brooklyn between Church Av and Jay St-MetroTech for two trips each rush hour period.",1,http://web.mta.info/nyct/service/pdf/tfcur.pdf,FF6319, 20 | FS,MTA NYCT,S,Franklin Avenue Shuttle,"Train provides full time connecting service between the A and C at the Franklin Av/Fulton St station, and the Q at the Prospect Park/Empire Blvd station. A free transfer is also available at the Botanic Garden/Eastern Parkway Station to the 2, 3, 4, and 5 service.",1,http://web.mta.info/nyct/service/pdf/tscur.pdf,, 21 | G,MTA NYCT,G,Brooklyn-Queens Crosstown,"Trains operate between Court Square, Queens and Church Av, Brooklyn on weekdays, late nights, and weekends.",1,http://web.mta.info/nyct/service/pdf/tgcur.pdf,6CBE45, 22 | J,MTA NYCT,J,Nassau St Local,"Trains operate weekdays between Jamaica Center (Parsons/Archer), Queens, and Broad St, Manhattan at all times. During weekdays, Trains going to Manhattan run express in Brooklyn between Myrtle Av and Marcy Av from about 7 AM to 1 PM and from Manhattan from 1:30 PM and 8 PM. During weekday rush hours, trains provide skip-stop service. Skip-stop service means that some stations are served by J trains, some stations are served by the Z trains, and some stations are served by both J and Z trains. J/Z skip-stop service runs towards Manhattan from about 7 AM to 8:15 AM and from Manhattan from about 4:30 PM to 5:45 PM.",1,http://web.mta.info/nyct/service/pdf/tjcur.pdf,996633, 23 | L,MTA NYCT,L,14 St-Canarsie Local,"Trains operate between 8 Av/14 St, Manhattan, and Rockaway Pkwy/Canarsie, Brooklyn, at all times.",1,http://web.mta.info/nyct/service/pdf/tlcur.pdf,A7A9AC, 24 | M,MTA NYCT,M,Queens Blvd Local/6 Av Local,"Trains operate between Middle Village-Metropolitan Avenue, Queens and Myrtle Avenue, Brooklyn at all times. Service is extended weekdays (except late nights) Continental Ave, Queens, All trains provide local service.",1,http://web.mta.info/nyct/service/pdf/tmcur.pdf,FF6319, 25 | N,MTA NYCT,N,Broadway Local,"Trains operate from Astoria-Ditmars Boulevard, Queens, to Stillwell Avenue, Brooklyn, at all times. N trains in Manhattan operate along Broadway and across the Manhattan Bridge to and from Brooklyn. Trains in Brooklyn operate along 4th Avenue, then through Borough Park and Gravesend. Trains typically operate local in Queens and express in Manhattan and Brooklyn. Late night trains operate local in Manhattan and to/from Brooklyn via Whitehall St, Manhattan. Weekends N trains operate local in Manhattan.",1,http://web.mta.info/nyct/service/pdf/tncur.pdf,FCCC0A, 26 | Q,MTA NYCT,Q,Broadway Express,"Trains operate between 96 St-2 Av, Manhattan, and Stillwell Av, Brooklyn at all times. Trains operate local in Brooklyn at all times. Train operate express in Manhattan at all times, except late nights when trains operate local in Manhattan.",1,http://web.mta.info/nyct/service/pdf/tqcur.pdf,FCCC0A, 27 | R,MTA NYCT,R,Broadway Local,"Trains operate local between Forest Hills-71 Av, Queens, and 95 St/4 Av, Brooklyn, at all times except late nights. During late nights, trains operate only in Brooklyn between 36 St and 95 St/4 Av.",1,http://web.mta.info/nyct/service/pdf/trcur.pdf,FCCC0A, 28 | H,MTA NYCT,S,Rockaway Park Shuttle,"Service operates at all times between Broad Channel, and Rockaway Park, Queens.",1,http://web.mta.info/nyct/service/pdf/thcur.pdf,, 29 | W,MTA NYCT,W,Broadway Local,"Trains operate from Astoria-Ditmars Boulevard, Queens, to Whitehall St, Manhattan, on weekdays only.",1,http://web.mta.info/nyct/service/pdf/twcur.pdf,FCCC0A, 30 | Z,MTA NYCT,Z,Nassau St Express,"Trains operate weekday rush hours only. During weekday rush hours, J and Z trains provide skip-stop service. Skip-stop service means that some stations are served by J trains, some stations are served by the Z trains, and some stations are served by both J and Z trains. J/Z skip-stop service runs towards Manhattan from about 7 AM to 8:15 AM and from Manhattan from about 4:30 PM to 5:45 PM.",1,http://web.mta.info/nyct/service/pdf/tjcur.pdf,996633, 31 | SI,MTA NYCT,SIR,Staten Island Railway,"Service runs 24 hours a day between the St George and Tottenville terminals. At the St George terminal, customers can make connections with Staten Island Ferry service to Manhattan.",2,http://web.mta.info/nyct/service/pdf/sircur.pdf,, 32 | -------------------------------------------------------------------------------- /scripts/data/google_transit/transfers.txt: -------------------------------------------------------------------------------- 1 | from_stop_id,to_stop_id,transfer_type,min_transfer_time 2 | 101,101,2,180 3 | 103,103,2,180 4 | 104,104,2,180 5 | 106,106,2,180 6 | 107,107,2,180 7 | 108,108,2,180 8 | 109,109,2,180 9 | 110,110,2,180 10 | 111,111,2,180 11 | 112,112,2,180 12 | 112,A09,2,180 13 | 113,113,2,180 14 | 114,114,2,180 15 | 115,115,2,180 16 | 116,116,2,180 17 | 117,117,2,180 18 | 118,118,2,180 19 | 119,119,2,180 20 | 120,120,2,180 21 | 121,121,2,180 22 | 122,122,2,180 23 | 123,123,2,0 24 | 124,124,2,180 25 | 125,125,2,180 26 | 125,A24,2,180 27 | 126,126,2,180 28 | 127,127,2,0 29 | 127,725,2,180 30 | 127,902,2,180 31 | 127,A27,2,300 32 | 127,R16,2,180 33 | 128,128,2,300 34 | 129,129,2,180 35 | 130,130,2,180 36 | 131,131,2,180 37 | 132,132,2,0 38 | 132,D19,2,300 39 | 132,L02,2,180 40 | 133,133,2,180 41 | 134,134,2,180 42 | 135,135,2,180 43 | 136,136,2,180 44 | 137,137,2,180 45 | 138,138,2,180 46 | 139,139,2,180 47 | 140,140,2,180 48 | 140,R27,2,120 49 | 201,201,2,180 50 | 204,204,2,180 51 | 205,205,2,180 52 | 206,206,2,180 53 | 207,207,2,180 54 | 208,208,2,180 55 | 209,209,2,180 56 | 210,210,2,180 57 | 211,211,2,180 58 | 212,212,2,180 59 | 213,213,2,180 60 | 214,214,2,180 61 | 215,215,2,180 62 | 216,216,2,180 63 | 217,217,2,180 64 | 218,218,2,180 65 | 219,219,2,180 66 | 220,220,2,180 67 | 221,221,2,180 68 | 222,222,2,180 69 | 222,415,2,180 70 | 227,227,2,0 71 | 228,228,2,180 72 | 228,A36,2,180 73 | 228,E01,2,180 74 | 228,R25,2,420 75 | 229,229,2,180 76 | 229,418,2,300 77 | 229,A38,2,180 78 | 229,M22,2,300 79 | 230,230,2,180 80 | 231,231,2,180 81 | 232,232,2,180 82 | 232,423,2,300 83 | 232,R28,2,180 84 | 233,233,2,180 85 | 234,234,2,0 86 | 235,235,2,300 87 | 235,D24,2,180 88 | 235,R31,2,180 89 | 236,236,2,180 90 | 237,237,2,180 91 | 238,238,2,180 92 | 239,239,2,0 93 | 239,S04,2,180 94 | 241,241,2,180 95 | 242,242,2,180 96 | 243,243,2,180 97 | 244,244,2,180 98 | 245,245,2,180 99 | 246,246,2,180 100 | 247,247,2,180 101 | 248,248,2,180 102 | 249,249,2,180 103 | 250,250,2,0 104 | 251,251,2,180 105 | 252,252,2,180 106 | 253,253,2,180 107 | 254,254,2,180 108 | 254,L26,2,300 109 | 255,255,2,180 110 | 256,256,2,180 111 | 257,257,2,180 112 | 301,301,2,180 113 | 302,302,2,180 114 | 401,401,2,180 115 | 402,402,2,180 116 | 405,405,2,180 117 | 406,406,2,180 118 | 407,407,2,180 119 | 408,408,2,180 120 | 409,409,2,0 121 | 410,410,2,180 122 | 411,411,2,180 123 | 412,412,2,180 124 | 413,413,2,180 125 | 414,414,2,180 126 | 414,D11,2,180 127 | 415,222,2,180 128 | 415,415,2,0 129 | 416,416,2,180 130 | 418,229,2,300 131 | 418,418,2,180 132 | 418,A38,2,180 133 | 418,M22,2,300 134 | 419,419,2,180 135 | 420,420,2,180 136 | 423,232,2,300 137 | 423,423,2,180 138 | 423,R28,2,180 139 | 501,501,2,180 140 | 502,502,2,180 141 | 503,503,2,180 142 | 504,504,2,180 143 | 505,505,2,180 144 | 601,601,2,180 145 | 602,602,2,180 146 | 603,603,2,180 147 | 604,604,2,180 148 | 606,606,2,180 149 | 607,607,2,180 150 | 608,608,2,0 151 | 609,609,2,180 152 | 610,610,2,180 153 | 611,611,2,180 154 | 612,612,2,180 155 | 613,613,2,0 156 | 614,614,2,180 157 | 615,615,2,180 158 | 616,616,2,180 159 | 617,617,2,180 160 | 618,618,2,180 161 | 619,619,2,0 162 | 621,621,2,180 163 | 622,622,2,180 164 | 623,623,2,180 165 | 624,624,2,180 166 | 625,625,2,180 167 | 626,626,2,180 168 | 627,627,2,180 169 | 628,628,2,180 170 | 629,629,2,180 171 | 629,B08,2,300 172 | 629,R11,2,180 173 | 630,630,2,180 174 | 630,F11,2,180 175 | 631,631,2,0 176 | 631,723,2,180 177 | 631,901,2,180 178 | 632,632,2,180 179 | 633,633,2,180 180 | 634,634,2,180 181 | 635,635,2,0 182 | 635,L03,2,180 183 | 635,R20,2,180 184 | 636,636,2,180 185 | 637,637,2,180 186 | 637,D21,2,180 187 | 638,638,2,180 188 | 639,639,2,180 189 | 639,M20,2,180 190 | 639,Q01,2,180 191 | 639,R23,2,180 192 | 640,640,2,0 193 | 640,M21,2,180 194 | 701,701,2,180 195 | 702,702,2,180 196 | 705,705,2,180 197 | 706,706,2,180 198 | 707,707,2,0 199 | 708,708,2,180 200 | 709,709,2,180 201 | 710,710,2,180 202 | 710,G14,2,180 203 | 711,711,2,180 204 | 712,712,2,0 205 | 713,713,2,180 206 | 714,714,2,180 207 | 715,715,2,180 208 | 716,716,2,180 209 | 718,718,2,0 210 | 718,R09,2,0 211 | 719,719,2,180 212 | 719,F09,2,300 213 | 719,G22,2,180 214 | 720,720,2,180 215 | 721,721,2,180 216 | 723,631,2,180 217 | 723,723,2,180 218 | 723,901,2,300 219 | 724,724,2,180 220 | 724,D16,2,180 221 | 725,127,2,180 222 | 725,725,2,180 223 | 725,902,2,300 224 | 725,A27,2,180 225 | 725,R16,2,180 226 | 901,631,2,180 227 | 901,723,2,300 228 | 901,901,2,180 229 | 902,127,2,180 230 | 902,725,2,300 231 | 902,902,2,180 232 | 902,A27,2,300 233 | 902,R16,2,180 234 | A02,A02,2,180 235 | A03,A03,2,180 236 | A05,A05,2,180 237 | A06,A06,2,180 238 | A07,A07,2,180 239 | A09,112,2,180 240 | A09,A09,2,0 241 | A10,A10,2,180 242 | A11,A11,2,180 243 | A12,A12,2,0 244 | A12,D13,2,180 245 | A14,A14,2,180 246 | A15,A15,2,0 247 | A16,A16,2,180 248 | A17,A17,2,180 249 | A18,A18,2,180 250 | A19,A19,2,180 251 | A20,A20,2,180 252 | A21,A21,2,180 253 | A22,A22,2,180 254 | A24,125,2,180 255 | A24,A24,2,0 256 | A25,A25,2,180 257 | A27,127,2,300 258 | A27,725,2,180 259 | A27,902,2,300 260 | A27,A27,2,0 261 | A27,R16,2,300 262 | A28,A28,2,300 263 | A30,A30,2,180 264 | A31,A31,2,0 265 | A31,L01,2,90 266 | A32,A32,2,0 267 | A32,D20,2,180 268 | A33,A33,2,180 269 | A34,A34,2,0 270 | A36,228,2,180 271 | A36,A36,2,180 272 | A36,E01,2,300 273 | A36,R25,2,420 274 | A38,229,2,180 275 | A38,418,2,180 276 | A38,A38,2,180 277 | A38,M22,2,180 278 | A40,A40,2,180 279 | A41,A41,2,180 280 | A41,R29,2,90 281 | A42,A42,2,180 282 | A43,A43,2,180 283 | A44,A44,2,180 284 | A45,A45,2,180 285 | A45,S01,2,180 286 | A46,A46,2,180 287 | A47,A47,2,180 288 | A48,A48,2,0 289 | A49,A49,2,180 290 | A50,A50,2,180 291 | A51,A51,2,0 292 | A51,J27,2,180 293 | A51,L22,2,180 294 | A52,A52,2,180 295 | A53,A53,2,180 296 | A54,A54,2,180 297 | A55,A55,2,0 298 | A57,A57,2,180 299 | A59,A59,2,180 300 | A60,A60,2,180 301 | A61,A61,2,180 302 | A63,A63,2,180 303 | A64,A64,2,180 304 | A65,A65,2,180 305 | B04,B04,2,180 306 | B06,B06,2,180 307 | B08,629,2,300 308 | B08,B08,2,180 309 | B08,R11,2,300 310 | B10,B10,2,180 311 | B12,B12,2,180 312 | B13,B13,2,180 313 | B14,B14,2,180 314 | B15,B15,2,180 315 | B16,B16,2,180 316 | B16,N04,2,180 317 | B17,B17,2,180 318 | B18,B18,2,180 319 | B19,B19,2,180 320 | B20,B20,2,180 321 | B21,B21,2,180 322 | B22,B22,2,180 323 | B23,B23,2,180 324 | D01,D01,2,180 325 | D03,D03,2,0 326 | D04,D04,2,0 327 | D05,D05,2,0 328 | D06,D06,2,180 329 | D07,D07,2,0 330 | D08,D08,2,180 331 | D09,D09,2,180 332 | D10,D10,2,180 333 | D11,414,2,180 334 | D11,D11,2,180 335 | D12,D12,2,180 336 | D13,A12,2,180 337 | D13,D13,2,0 338 | D14,D14,2,180 339 | D15,D15,2,300 340 | D16,724,2,180 341 | D16,D16,2,0 342 | D17,D17,2,0 343 | D17,R17,2,180 344 | D18,D18,2,180 345 | D19,132,2,300 346 | D19,D19,2,180 347 | D19,L02,2,180 348 | D20,A32,2,180 349 | D20,D20,2,0 350 | D21,637,2,180 351 | D21,D21,2,0 352 | D22,D22,2,180 353 | D24,235,2,180 354 | D24,D24,2,180 355 | D24,R31,2,300 356 | D25,D25,2,180 357 | D26,D26,2,180 358 | D27,D27,2,180 359 | D28,D28,2,0 360 | D29,D29,2,180 361 | D30,D30,2,180 362 | D31,D31,2,0 363 | D32,D32,2,180 364 | D33,D33,2,180 365 | D34,D34,2,180 366 | D35,D35,2,0 367 | D37,D37,2,180 368 | D38,D38,2,180 369 | D39,D39,2,0 370 | D40,D40,2,300 371 | D41,D41,2,180 372 | D42,D42,2,180 373 | D43,D43,2,180 374 | E01,228,2,180 375 | E01,A36,2,300 376 | E01,R25,2,240 377 | E01,E01,2,180 378 | F01,F01,2,180 379 | F02,F02,2,180 380 | F03,F03,2,180 381 | F04,F04,2,180 382 | F06,F06,2,0 383 | F07,F07,2,180 384 | F09,719,2,300 385 | F09,F09,2,180 386 | F09,G22,2,180 387 | F11,630,2,180 388 | F11,F11,2,180 389 | F12,F12,2,180 390 | F14,F14,2,300 391 | F15,F15,2,180 392 | F15,M18,2,180 393 | F16,F16,2,180 394 | F18,F18,2,180 395 | F21,F21,2,180 396 | F22,F22,2,180 397 | F23,F23,2,180 398 | F23,R33,2,180 399 | F24,F24,2,0 400 | F25,F25,2,180 401 | F26,F26,2,180 402 | F27,F27,2,0 403 | F29,F29,2,180 404 | F30,F30,2,180 405 | F31,F31,2,180 406 | F32,F32,2,180 407 | F33,F33,2,180 408 | F34,F34,2,180 409 | F35,F35,2,180 410 | F36,F36,2,180 411 | F38,F38,2,180 412 | F39,F39,2,180 413 | G05,G05,2,180 414 | G06,G06,2,180 415 | G07,G07,2,180 416 | G08,G08,2,0 417 | G09,G09,2,180 418 | G10,G10,2,180 419 | G11,G11,2,180 420 | G12,G12,2,180 421 | G13,G13,2,180 422 | G14,710,2,180 423 | G14,G14,2,180 424 | G15,G15,2,180 425 | G16,G16,2,180 426 | G18,G18,2,180 427 | G19,G19,2,180 428 | G20,G20,2,180 429 | G21,G21,2,180 430 | G22,719,2,180 431 | G22,F09,2,180 432 | G22,G22,2,180 433 | G24,G24,2,180 434 | G26,G26,2,180 435 | G28,G28,2,180 436 | G29,G29,2,180 437 | G29,L10,2,180 438 | G30,G30,2,180 439 | G31,G31,2,180 440 | G32,G32,2,180 441 | G33,G33,2,180 442 | G34,G34,2,180 443 | G35,G35,2,180 444 | G36,G36,2,180 445 | H02,H02,2,180 446 | H03,H03,2,180 447 | H04,H04,2,180 448 | H06,H06,2,180 449 | H07,H07,2,180 450 | H08,H08,2,180 451 | H09,H09,2,180 452 | H10,H10,2,180 453 | H11,H11,2,180 454 | H12,H12,2,180 455 | H13,H13,2,180 456 | H14,H14,2,180 457 | H15,H15,2,180 458 | J12,J12,2,180 459 | J13,J13,2,180 460 | J14,J14,2,180 461 | J15,J15,2,180 462 | J16,J16,2,180 463 | J17,J17,2,180 464 | J19,J19,2,180 465 | J20,J20,2,180 466 | J21,J21,2,180 467 | J22,J22,2,180 468 | J23,J23,2,180 469 | J24,J24,2,180 470 | J27,A51,2,180 471 | J27,J27,2,0 472 | J27,L22,2,180 473 | J28,J28,2,180 474 | J29,J29,2,180 475 | J30,J30,2,180 476 | J31,J31,2,180 477 | L01,A31,2,90 478 | L01,L01,2,180 479 | L02,132,2,180 480 | L02,D19,2,180 481 | L02,L02,2,180 482 | L03,635,2,180 483 | L03,L03,2,180 484 | L03,R20,2,180 485 | L05,L05,2,180 486 | L06,L06,2,180 487 | L08,L08,2,180 488 | L10,G29,2,180 489 | L10,L10,2,180 490 | L11,L11,2,180 491 | L12,L12,2,180 492 | L13,L13,2,180 493 | L14,L14,2,180 494 | L15,L15,2,180 495 | L16,L16,2,180 496 | L17,L17,2,180 497 | L17,M08,2,180 498 | L19,L19,2,180 499 | L20,L20,2,180 500 | L21,L21,2,180 501 | L22,A51,2,180 502 | L22,J27,2,180 503 | L22,L22,2,180 504 | L24,L24,2,180 505 | L25,L25,2,180 506 | L26,254,2,300 507 | L26,L26,2,180 508 | L27,L27,2,180 509 | L28,L28,2,180 510 | L29,L29,2,180 511 | M01,M01,2,180 512 | M04,M04,2,180 513 | M05,M05,2,180 514 | M06,M06,2,180 515 | M08,L17,2,180 516 | M08,M08,2,180 517 | M09,M09,2,180 518 | M10,M10,2,180 519 | M11,M11,2,0 520 | M12,M12,2,180 521 | M13,M13,2,180 522 | M14,M14,2,180 523 | M16,M16,2,0 524 | M18,F15,2,180 525 | M18,M18,2,180 526 | M19,M19,2,180 527 | M20,639,2,180 528 | M20,M20,2,180 529 | M20,Q01,2,180 530 | M20,R23,2,300 531 | M21,640,2,180 532 | M21,M21,2,180 533 | M22,229,2,300 534 | M22,418,2,300 535 | M22,A38,2,180 536 | M22,M22,2,180 537 | M23,M23,2,180 538 | N02,N02,2,180 539 | N03,N03,2,180 540 | N04,B16,2,180 541 | N04,N04,2,180 542 | N05,N05,2,180 543 | N06,N06,2,180 544 | N07,N07,2,180 545 | N08,N08,2,180 546 | N09,N09,2,180 547 | N10,N10,2,180 548 | Q01,639,2,180 549 | Q01,M20,2,180 550 | Q01,Q01,2,180 551 | Q01,R23,2,180 552 | R01,R01,2,180 553 | R03,R03,2,0 554 | R04,R04,2,180 555 | R05,R05,2,180 556 | R06,R06,2,180 557 | R08,R08,2,180 558 | R09,718,2,0 559 | R09,R09,2,180 560 | R11,629,2,180 561 | R11,B08,2,300 562 | R11,R11,2,0 563 | R13,R13,2,180 564 | R14,R14,2,180 565 | R15,R15,2,180 566 | R16,127,2,180 567 | R16,725,2,180 568 | R16,902,2,180 569 | R16,A27,2,300 570 | R16,R16,2,0 571 | R17,D17,2,180 572 | R17,R17,2,0 573 | R18,R18,2,180 574 | R19,R19,2,180 575 | R20,635,2,180 576 | R20,L03,2,180 577 | R20,R20,2,0 578 | R21,R21,2,180 579 | R22,R22,2,180 580 | R23,639,2,180 581 | R23,M20,2,300 582 | R23,Q01,2,180 583 | R23,R23,2,180 584 | R24,R24,2,180 585 | R25,E01,2,240 586 | R25,A36,2,420 587 | R25,228,2,420 588 | R26,R26,2,180 589 | R27,R27,2,180 590 | R27,140,2,120 591 | R28,232,2,180 592 | R28,423,2,180 593 | R28,R28,2,180 594 | R29,R29,2,180 595 | R29,A41,2,90 596 | R30,R30,2,180 597 | R31,235,2,180 598 | R31,D24,2,300 599 | R31,R31,2,0 600 | R32,R32,2,180 601 | R33,F23,2,180 602 | R33,R33,2,180 603 | R34,R34,2,180 604 | R35,R35,2,180 605 | R36,R36,2,0 606 | R39,R39,2,180 607 | R40,R40,2,180 608 | R41,R41,2,0 609 | R42,R42,2,180 610 | R43,R43,2,180 611 | R44,R44,2,180 612 | R45,R45,2,180 613 | S01,A45,2,180 614 | S01,S01,2,180 615 | S03,S03,2,180 616 | S04,239,2,180 617 | S04,S04,2,180 618 | -------------------------------------------------------------------------------- /scripts/gcs_utils.py: -------------------------------------------------------------------------------- 1 | from google.cloud import storage 2 | from typing import List 3 | import pandas as pd 4 | import os 5 | 6 | data_clinic_bucket = 'mta_crowding_data' 7 | 8 | class gcs_util: 9 | """ 10 | Convenience class for managing google cloud storage. 11 | Really more out of laziness than necessity. 12 | """ 13 | def __init__(self, bucket_path: str = data_clinic_bucket): 14 | self.client = storage.Client.create_anonymous_client() 15 | self.bucket = self.client.bucket(bucket_path) 16 | 17 | 18 | def list_dirs(self, dir:str = None): 19 | """ 20 | Lists only directories in the blob name, not blobs 21 | list_prefixes(prefix="foodir/") 22 | """ 23 | if not (dir is None or dir.endswith("/")): 24 | dir += "/" 25 | 26 | iterator = self.bucket.list_blobs(prefix=dir,delimiter="/") 27 | response = iterator._get_next_page_response() 28 | if 'prefixes' in response: 29 | return response['prefixes'] 30 | return [] 31 | 32 | def list_blobs(self, dir:str = None): 33 | """ 34 | Lists all blobs 35 | list_blobs(prefix="foodir/") 36 | """ 37 | return list(self.bucket.list_blobs(prefix=dir)) 38 | 39 | def upload_dataframe(self, df: pd.DataFrame, blob_path:str): 40 | temp_path = 'df.pkl' 41 | try: 42 | df.to_pickle('df.pkl') 43 | self.upload_blob(blob_path, temp_path) 44 | finally: 45 | os.remove(temp_path) 46 | 47 | def upload_blob(self, blob_path: str, file_path: str): 48 | blob = self.bucket.blob(blob_path) 49 | blob.upload_from_filename(file_path) 50 | 51 | def read_dataframe(self, blob_path: str) -> pd.DataFrame: 52 | temp_path = 'temp.parquet' 53 | blob = self.get_blob(blob_path) 54 | try: 55 | blob.download_to_filename(temp_path) 56 | return pd.read_parquet(temp_path) 57 | finally: 58 | if os.path.exists(temp_path): 59 | os.remove(temp_path) 60 | 61 | def get_blob(self, blob_path: str) -> str: 62 | return self.bucket.get_blob(blob_path) 63 | 64 | 65 | def delete_blob(self, blob_path: str): 66 | blob = self.bucket.get_blob(blob_path) 67 | blob.delete() 68 | 69 | def delete_blob_dir(self, blob_prefix: str) -> List[str]: 70 | deleted_blobs = [] 71 | for blob in self.bucket.list_blobs(prefix=blob_prefix): 72 | deleted_blobs.append(blob.path) 73 | blob.delete() 74 | return deleted_blobs -------------------------------------------------------------------------------- /scripts/gtfs.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import sys, logging 4 | from datetime import datetime, timedelta 5 | import matplotlib.pyplot as plt 6 | import re 7 | import os 8 | import turnstile 9 | from gcs_utils import gcs_util 10 | gcs = gcs_util() 11 | 12 | 13 | def get_schedule(lt, days=15): 14 | ''' 15 | Get daily processed realtime GTFS data 16 | ''' 17 | dates = [lt - timedelta(days=i) for i in range(1,days+1)] 18 | keep_dates = [re.sub('-','',str(d.date())) for d in dates] 19 | date_pattern = re.compile("realtime/daily/vehicle_updates_([0-9]*)") 20 | loaded_files = [] 21 | df = pd.DataFrame() 22 | 23 | for blob in gcs.list_blobs('realtime/daily/'): 24 | name = blob.name 25 | if bool(re.search("realtime/daily/vehicle_updates_", name)): 26 | date = date_pattern.match(name)[1] 27 | if date in keep_dates: 28 | loaded_files.append(blob.name) 29 | df_temp = gcs.read_dataframe(name) 30 | df = df.append(df_temp,sort=False).drop_duplicates() 31 | 32 | return df, dates 33 | 34 | def keep_latest_info(df): 35 | ''' 36 | Function to keep latest available information for train arrivals at the station along a trip 37 | ''' 38 | df = df.sort_values('timestamp') 39 | df = df.drop_duplicates(subset=['stop_id'],keep='last') 40 | return df 41 | 42 | def process_schedule(df_raw, st, en, stop_order_merged): 43 | ''' 44 | Processing raw-gtfs data into schedule used for crowding estimation. 45 | - Removing unused columns 46 | - Filling in missing start times 47 | - Imputing missing starting stops 48 | - Dropping trips which have >75% stops missing compared to a typical trip on that route 49 | ''' 50 | ## basic cleaning: retaining actual stops, filling nulls, removing duplicates 51 | cols_to_remove = ['id','alert.header_text.translation','alert.informed_entity','stop_headsign','pickup_type','drop_off_type','shape_dist_traveled','departure_time'] 52 | cols_to_remove = list(set(df_raw.columns).intersection(set(cols_to_remove))) 53 | if len(cols_to_remove) > 0: 54 | df_raw = df_raw.drop(columns=cols_to_remove,axis=1) 55 | df_raw = df_raw.drop_duplicates(subset=['current_status','current_stop_sequence','route_id','start_date','start_time','stop_id','stop_name','timestamp','trip_id']) 56 | df_raw.timestamp = pd.to_datetime(df_raw.timestamp) 57 | df_raw = df_raw[df_raw.timestamp.notnull()] 58 | 59 | df_raw = df_raw[~df_raw.route_id.isin(['FS','H','GS'])] 60 | df_raw.route_id = [re.sub('X$','',r) for r in df_raw.route_id] 61 | 62 | trimmed_dfs = [] 63 | df_raw.groupby(['start_date','trip_id']).apply( 64 | lambda g: trimmed_dfs.append(keep_latest_info(g))) 65 | 66 | df = pd.concat(trimmed_dfs) 67 | 68 | df.timestamp = df.timestamp.dt.tz_localize('UTC').dt.tz_convert('US/Eastern').dt.tz_localize(None) 69 | df.start_time = pd.to_datetime(df.start_time) 70 | df.start_time = df.start_time.dt.tz_localize('UTC').dt.tz_convert('US/Eastern').dt.tz_localize(None) 71 | 72 | first_time = df.groupby(['start_date','trip_id']).timestamp.min().to_frame().reset_index().rename(columns={'timestamp':'first_time'}) 73 | df = df.merge(first_time,how='left',on=['start_date','trip_id']) 74 | df['direction'] = [re.search("(N|S)$",x)[0] if re.search("(N|S)$",x) else None for x in df.stop_id] 75 | df.loc[df.direction.isnull(),'direction'] = [re.search("(N|S)$",x)[0] if re.search("(N|S)$",x) else None for x in df[df.direction.isnull()].trip_id] 76 | df['st_time'] = [y if pd.isna(x) else x for x,y in zip(df.start_time,df.first_time)] 77 | 78 | ## defining unique ID for a trip, identifying if a trip is truncated or only has a single stop 79 | df['uid'] = df.groupby(['route_id','direction','start_date','st_time','trip_id']).grouper.group_info[0] 80 | df = df[df.start_date != (st-timedelta(days=1)).strftime('%Y%m%d')] 81 | trip_counts = df.groupby(['uid','route_id','direction']).count().reset_index() 82 | single_stop_trips = trip_counts[trip_counts.timestamp == 1].uid 83 | df = df[~df.uid.isin(single_stop_trips)] 84 | end_times = df.groupby('uid').timestamp.max().to_frame().reset_index() 85 | end_times['legitimate_trunc'] = end_times.timestamp > (en + timedelta(hours=23, minutes=50)) 86 | df = df.merge(end_times[['uid','legitimate_trunc']],how='left',on='uid') 87 | 88 | ### Imputation goes here 89 | imputed_dfs = [] 90 | df.groupby(['start_date','trip_id']).apply( 91 | lambda g: imputed_dfs.append(add_starting_stations(g,stop_order_merged))) 92 | 93 | df = pd.concat(imputed_dfs) 94 | 95 | ## getting max trip length and using it to filter the more complete trips 96 | max_trip_length = df.groupby(['uid','route_id','direction']).current_stop_sequence.max().reset_index().groupby(['route_id','direction']).max().reset_index().rename(columns={'current_stop_sequence':'max_trip_length'}) 97 | df = df.merge(max_trip_length[['route_id','direction','max_trip_length']],how='left',on=['route_id','direction']) 98 | trip_length = df.groupby(['uid','route_id','direction']).count().reset_index().rename(columns={'timestamp':'trip_length'}) 99 | df = df.merge(trip_length[['uid','trip_length']],how='left',on='uid') 100 | df['pct_max'] = df.trip_length/df.max_trip_length 101 | df = df[~((df.pct_max < 0.24) &(df.legitimate_trunc == False))] 102 | 103 | df['time'] = pd.to_datetime(df.timestamp) 104 | df.time = df.time.dt.floor('T') 105 | df['trimmed_stop_id'] = [re.sub(r'(N|S)$','',x) for x in df.stop_id] 106 | df['direction_id'] = [1 if x == 'S' else 0 for x in df.direction] 107 | 108 | df['hour'] = df.time.dt.hour 109 | return df 110 | 111 | 112 | def add_starting_stations(df,stop_order_merged): 113 | ''' 114 | For each trip, adds the starting stop which is typically missing in the raw realtime GTFS data 115 | ''' 116 | trip_df = df.copy() 117 | trip_df = keep_latest_info(trip_df) 118 | trip_df = trip_df.sort_values('timestamp') 119 | trip_df = trip_df.reset_index(drop=True) 120 | stops = trip_df.stop_id 121 | stop_seq = trip_df.current_stop_sequence 122 | min_seq = min(stop_seq) 123 | if np.isnan(min_seq): 124 | return trip_df 125 | else: 126 | first_stop = stops[stop_seq.idxmin()] 127 | 128 | if (first_stop[-1] != 'N')&(first_stop[-1] != 'S'): 129 | first_stop = first_stop + 'S' 130 | try: 131 | first_stop_order = get_order_for_stop(re.sub('N$','S',first_stop),list(trip_df.route_id)[0],list(trip_df.direction)[0],stop_order_merged) 132 | except: 133 | print("{}-{}-{}".format(first_stop,list(trip_df.route_id)[0],list(trip_df.direction)[0])) 134 | 135 | try: 136 | correct_first_stop = get_stop(stop_order_merged,list(trip_df.route_id)[0],list(trip_df.direction)[0],order=1) 137 | except: 138 | print("{}-{}".format(list(trip_df.route_id)[0],list(trip_df.direction)[0])) 139 | return trip_df 140 | 141 | 142 | if (min_seq > 4) | (first_stop == correct_first_stop) | (first_stop_order > 4) : 143 | pass 144 | else: 145 | previous_stops = get_previous_stops(stop_order_merged,list(trip_df.route_id)[0],list(trip_df.direction)[0],first_stop) 146 | row_to_duplicate = trip_df[trip_df.stop_id == first_stop].copy() 147 | i = 1 148 | for p in previous_stops: 149 | new_row = row_to_duplicate.copy() 150 | new_row['stop_id'] = p 151 | new_row['timestamp'] = new_row['timestamp'] - timedelta(minutes=2*i) 152 | new_row['stop_name'] = get_name_for_stop_id(stop_order_merged, p) 153 | new_row['current_stop_sequence'] = new_row['current_stop_sequence'] - i 154 | trip_df = trip_df.append(new_row, sort=False) 155 | i = i+1 156 | 157 | return trip_df 158 | 159 | def get_stop(stop_order,route_id, direction, order=1): 160 | stops = stop_order[stop_order.route_id == route_id] 161 | max_seq = stops.order.max() 162 | if direction == 'S': 163 | stop_id = list(stops[stops.order == order].stop_id) 164 | else: 165 | stop_id = list(stops[stops.order == max_seq-order+1].stop_id) 166 | stop_id = [re.sub('S$','N',x) for x in stop_id] 167 | 168 | return stop_id[0] 169 | 170 | def get_previous_stops(stop_order,route_id, direction, current_stop): 171 | stops = stop_order[stop_order.route_id == route_id] 172 | if re.sub('N$','S',current_stop) not in list(stops.stop_id): 173 | return [] 174 | max_seq = stops.order.max() 175 | if (current_stop[-1] != 'N')&(current_stop[-1] != 'S'): 176 | current_stop = current_stop + 'S' 177 | if direction == 'S': 178 | current_order = list(stops[stops.stop_id == current_stop].order) 179 | previous_stops = stops[stops.order < current_order[0]].sort_values('order', ascending=False) 180 | previous_stops = list(previous_stops.stop_id) 181 | else: 182 | current_order = list(stops[stops.stop_id == re.sub('N$','S',current_stop)].order) 183 | previous_stops = stops[stops.order > current_order[0]].sort_values('order') 184 | previous_stops = [re.sub('S$','N',x) for x in previous_stops.stop_id] 185 | 186 | return previous_stops 187 | 188 | def get_name_for_stop_id(stop_order_merged, stop_id): 189 | return list(stop_order_merged[stop_order_merged.stop_id == re.sub('N$','S',stop_id)].station)[0] 190 | 191 | def get_order_for_stop(stop_id,route_id,direction,stop_order): 192 | stops = stop_order[stop_order.route_id == route_id] 193 | if stop_id not in list(stops.stop_id): 194 | return 99 195 | else: 196 | max_seq = stops.order.max() 197 | if direction == 'S': 198 | current_order = list(stops[stops.stop_id == stop_id].order)[0] 199 | else: 200 | current_order = max_seq - list(stops[stops.stop_id == re.sub('N$','S',stop_id)].order)[0] + 1 201 | 202 | return current_order 203 | 204 | def fix_weird_times(x,add=1): 205 | units = x.split(':') 206 | h = int(units[0]) 207 | m = units[1] 208 | s = units[2] 209 | if h >= 24: 210 | h = h - 24 211 | 212 | y = str(h)+':'+m+':'+s 213 | fixed_time = pd.to_datetime(y,format='%H:%M:%S') 214 | fixed_time = fixed_time + timedelta(days=add) 215 | return fixed_time 216 | 217 | def process_static_schedule(df, st, en): 218 | ''' 219 | Function that processes GTFS Static Data used as aproxy for schedule last year 220 | - Removes unused columns 221 | - Replicates for each day of time-period of interest and adjusts dates accordingly 222 | ''' 223 | ## basic cleaning: retaining actual stops, filling nulls, removing duplicates 224 | cols_to_remove = ['id','alert.header_text.translation','alert.informed_entity','stop_headsign','pickup_type','drop_off_type', 225 | 'shape_dist_traveled','departure_time','service_id', 'trip_headsign','block_id','shape_id', 'stop_code','stop_desc', 'stop_lat', 226 | 'stop_lon', 'zone_id', 'stop_url', 'location_type','parent_station'] 227 | cols_to_remove = list(set(df.columns).intersection(set(cols_to_remove))) 228 | if len(cols_to_remove) > 0: 229 | df = df.drop(columns=cols_to_remove,axis=1) 230 | df = df.drop_duplicates() 231 | df['time'] = pd.to_datetime(df.arrival_time,format='%H:%M:%S',errors='coerce') 232 | df.loc[df.time.isnull(),'time'] = df[df.time.isnull()].arrival_time.apply(fix_weird_times) 233 | df['trimmed_stop_id'] = [re.sub(r'(N|S)$','',x) for x in df.stop_id] 234 | 235 | days_diff = (en-st).days 236 | last_year_dates = [st + timedelta(days=i) for i in range(0,days_diff+1)] 237 | clean_static_schedule = [] 238 | for d in last_year_dates: 239 | days_to_add = (d - datetime(year=1900,month=1,day=1)).days 240 | if d.weekday() < 5: 241 | ## weekday 242 | tmp_df = df[df.trip_id.str.contains('Weekday')] .copy() 243 | tmp_df['start_date'] = d.strftime(format="%Y%m%d") 244 | elif d.weekday() == 5: 245 | ## saturday 246 | tmp_df = df[df.trip_id.str.contains('Saturday')] .copy() 247 | tmp_df['start_date'] = d.strftime(format="%Y%m%d") 248 | else: 249 | ## sunday 250 | tmp_df = df[df.trip_id.str.contains('Sunday')] .copy() 251 | tmp_df['start_date'] = d.strftime(format="%Y%m%d") 252 | 253 | tmp_df.time = tmp_df.time + timedelta(days=days_to_add) 254 | clean_static_schedule.append(tmp_df) 255 | 256 | clean_static_schedule = pd.concat(clean_static_schedule,ignore_index=True, sort=False) 257 | first_time = clean_static_schedule.groupby(['start_date','trip_id']).time.min().to_frame().reset_index().rename(columns={'time':'first_time'}) 258 | clean_static_schedule = clean_static_schedule.merge(first_time,how='left',on=['start_date','trip_id']) 259 | 260 | ## defining unique ID for a trip 261 | clean_static_schedule['uid'] = clean_static_schedule.groupby(['route_id','direction_id','start_date','first_time','trip_id']).grouper.group_info[0] 262 | 263 | clean_static_schedule.time = clean_static_schedule.time.dt.floor('T') 264 | clean_static_schedule['hour'] = clean_static_schedule.time.dt.hour 265 | return clean_static_schedule 266 | 267 | -------------------------------------------------------------------------------- /scripts/heuristics.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import sys, logging 4 | from datetime import datetime, timedelta 5 | import matplotlib.pyplot as plt 6 | import re 7 | import os 8 | import bisect 9 | import io 10 | import logging 11 | import re 12 | import requests 13 | 14 | 15 | def interpolate_for_missing_stations(group): 16 | ''' 17 | Filling missing entry and exit weights in cases of service change 18 | ''' 19 | group['estimated_entries'] = group.estimated_entries.interpolate(method='linear') 20 | group['estimated_exits'] = group.estimated_exits.interpolate(method='linear') 21 | return group 22 | 23 | def turnstile_weighted_entry_weights(ts,stop_order_merged): 24 | ''' 25 | Creating weights for entry and exit for all combinations of route and direction at a stop based for every hour using turnstile exits. 26 | ''' 27 | 28 | ts['hour'] = ts.datetime.dt.hour 29 | 30 | ts = ts.groupby(['STATION','LINENAME','modified_linename','hour']).sum()[['estimated_entries','estimated_exits']].reset_index() 31 | stop_order = [] 32 | for h in range(0,24): 33 | tmp = stop_order_merged.copy() 34 | tmp['hour'] = h 35 | stop_order.append(tmp) 36 | stop_order = pd.concat(stop_order,ignore_index=True,sort=False) 37 | exit_weighted_stop_weights = stop_order.merge(ts,how='left',right_on=['STATION','LINENAME','hour'],left_on=['id','LINENAME','hour']) 38 | exit_weighted_stop_weights = exit_weighted_stop_weights.sort_values(['route_id','hour','order']).groupby(['route_id','hour']).apply(interpolate_for_missing_stations) 39 | 40 | exit_weighted_stop_weights = exit_weighted_stop_weights.sort_values('order') 41 | exit_weighted_stop_weights['total_exits'] = exit_weighted_stop_weights.groupby(['route_id','hour'])['estimated_exits'].cumsum() 42 | min_exits = exit_weighted_stop_weights.groupby(['route_id','hour']).total_exits.min().reset_index().rename(columns={'total_exits':'min_exits'}) 43 | max_exits = exit_weighted_stop_weights.groupby(['route_id','hour']).total_exits.max().reset_index().rename(columns={'total_exits':'max_exits'}) 44 | exit_weighted_stop_weights = exit_weighted_stop_weights.merge(min_exits,on=['route_id','hour']).merge(max_exits,on=['route_id','hour']).sort_values(['route_id','hour','order']) 45 | exit_weighted_stop_weights['exits_before'] = exit_weighted_stop_weights.total_exits - exit_weighted_stop_weights.min_exits 46 | exit_weighted_stop_weights['exits_after'] = exit_weighted_stop_weights.max_exits - exit_weighted_stop_weights.total_exits 47 | exit_weighted_stop_weights['north_bound_entry_weight'] = exit_weighted_stop_weights.exits_before/(exit_weighted_stop_weights.max_exits-exit_weighted_stop_weights.min_exits) 48 | exit_weighted_stop_weights['south_bound_entry_weight'] = 1 - exit_weighted_stop_weights.exits_before/(exit_weighted_stop_weights.max_exits-exit_weighted_stop_weights.min_exits) 49 | exit_weighted_stop_weights['direction_id'] = 1 50 | exit_weighted_stop_weights['trimmed_stop_id'] = [re.sub(r'(N|S)$','',x) for x in exit_weighted_stop_weights.stop_id] 51 | exit_weighted_stop_weights_north = exit_weighted_stop_weights.copy() 52 | exit_weighted_stop_weights_north['direction_id'] = 0 53 | 54 | stop_weights = pd.concat([exit_weighted_stop_weights,exit_weighted_stop_weights_north],ignore_index=True,sort=False) 55 | stop_weights['entry_weight'] = [s if d ==1 else n for d,s,n in zip(stop_weights.direction_id,stop_weights.south_bound_entry_weight,stop_weights.north_bound_entry_weight)] 56 | stop_weights['exit_weight'] = 1- stop_weights.entry_weight 57 | 58 | total_weight = stop_weights.groupby(['STATION','modified_linename','direction_id','hour']).sum()[['entry_weight','exit_weight']].reset_index().rename(columns={'entry_weight':'total_entry_weight','exit_weight':'total_exit_weight'}) 59 | stop_weights = stop_weights.merge(total_weight,how='left',on=['STATION','modified_linename','direction_id','hour']) 60 | stop_weights['normalized_entry_weight'] = [x if x==y else x/y for x,y in zip(stop_weights.entry_weight,stop_weights.total_entry_weight)] 61 | stop_weights['normalized_exit_weight'] = [x if x==y else x/y for x,y in zip(stop_weights.exit_weight,stop_weights.total_exit_weight)] 62 | 63 | stop_weights.drop(columns=['exit_weight','entry_weight','total_entry_weight','total_exit_weight'], inplace=True) 64 | stop_weights.rename(columns={'normalized_entry_weight':'entry_weight','normalized_exit_weight':'exit_weight'},inplace=True) 65 | return stop_weights[['trimmed_stop_id','route_id','direction_id','hour','entry_weight','exit_weight']] 66 | 67 | 68 | def get_schedule_with_weights(turnstile,clean_df,stop_order_merged): 69 | ''' 70 | Adding entry and exit weights to the cleaned schedule data 71 | ''' 72 | stop_weights = turnstile_weighted_entry_weights(turnstile, stop_order_merged) 73 | clean_df = clean_df[~(clean_df.route_id.isin(['SI','GS','FS','H','L']))] 74 | schedule = clean_df.merge(stop_weights,how='left',on=['hour','trimmed_stop_id','route_id','direction_id']) 75 | mean_weights_test = stop_weights.groupby(['trimmed_stop_id','hour','direction_id']).mean()[['entry_weight','exit_weight']] 76 | mean_weights_test.reset_index(inplace=True) 77 | schedule_missing = schedule[schedule.entry_weight.isna()].copy() 78 | schedule_not_missing = schedule[~schedule.entry_weight.isna()].copy() 79 | schedule_missing = schedule_missing.drop(columns=['entry_weight','exit_weight']) 80 | schedule_missing = schedule_missing.merge(mean_weights_test,how='left',on=['trimmed_stop_id','hour','direction_id']) 81 | schedule = pd.concat([schedule_not_missing,schedule_missing],ignore_index=True,sort=False) 82 | schedule.loc[schedule.entry_weight.isna(),['entry_weight','exit_weight']] = [0.5,0.5] 83 | return schedule 84 | 85 | 86 | def merge_turnstile_schedule(turnstile_data,crosswalk,schedule): 87 | ''' 88 | Merging cleaned Turnstile data and processed GTFS schedule data 89 | ''' 90 | turnstile_data = turnstile_data.merge(crosswalk[['turnstile_station_name','turnstile_lines','gtfs_station_name','gtfs_lines','gtfs_stop_id']],how='left',left_on=['STATION','LINENAME'],right_on=['turnstile_station_name','turnstile_lines']) 91 | turnstile_data = turnstile_data[turnstile_data.gtfs_station_name.notnull()] 92 | turnstile_data = turnstile_data[turnstile_data.STATION.notnull()] 93 | turnstile_data = turnstile_data.drop_duplicates(subset=['datetime','STATION','gtfs_stop_id']) 94 | crowding = turnstile_data.merge(schedule,how='left',left_on=['gtfs_stop_id','datetime'],right_on=['trimmed_stop_id','time']) 95 | crowding.drop(['turnstile_station_name', 'turnstile_lines', 'trimmed_stop_id'],axis=1,inplace=True) 96 | return crowding 97 | 98 | 99 | def get_people_waiting(df): 100 | ''' 101 | Iterating through each station over time to get number of people waiting at the station 102 | ''' 103 | waiting = [0]*len(df) 104 | train_entries = [0]*len(df) 105 | idx = 0 106 | for i,row in df.iterrows(): 107 | if idx > 0: 108 | if row.time.date != prev_date: 109 | first_5am = True 110 | prev_date = row.time.date 111 | if (first_5am==True) & (row.hour_y >= 4) & (row.hour_y <= 6): 112 | waiting[idx] = max(0,waiting[idx-1] - train_entries[idx-1] + row.total_entries_since_last_train - row.total_exits_since_last_train) 113 | first_5am = False 114 | else: 115 | waiting[idx] = waiting[idx-1] - train_entries[idx-1] + row.total_entries_since_last_train 116 | else: 117 | cur_date = row.time.date 118 | prev_date = row.time.date 119 | first_5am = True 120 | 121 | train_entries[idx] = np.ceil(waiting[idx] * row.entry_weight) 122 | idx = idx +1 123 | df['waiting'] = waiting 124 | df['train_entries'] = train_entries 125 | 126 | return(df) 127 | 128 | def get_train_crowd(df, entry_exit_ratio=1.1): 129 | ''' 130 | Based on number of people waiting at the station, iterates along trip_id to get number of peopl entering and exiting from the train for crowd estimation 131 | ''' 132 | crowd = [0]*len(df) 133 | exits = [0]*len(df) 134 | idx = 0 135 | for i,row in df.iterrows(): 136 | if idx > 0: 137 | exits_per_line = row.exit_weight if len(row.modified_linename) > 1 else 1 138 | exits[idx] = int(min(row.total_exits_before_next_train*exits_per_line*entry_exit_ratio,crowd[idx-1])) 139 | crowd[idx] = crowd[idx-1] - exits[idx] + row.train_entries 140 | else: 141 | exits[idx] = 0 142 | crowd[idx] = row.train_entries 143 | idx = idx +1 144 | 145 | exits[idx-1] = crowd[idx-1] + exits[idx-1] 146 | df['crowd'] = crowd 147 | df['train_exits'] = exits 148 | return(df) 149 | 150 | def get_crowd_by_station_line(crowding, entry_exit_ratio=1.1): 151 | ''' 152 | Function that takes in merged GTFS and turnstile data and returns crowd for each stop along each trip 153 | ''' 154 | logging.getLogger().info("Starting") 155 | crowding = crowding[crowding.trip_id.notnull()] 156 | crowding = crowding.sort_values(['time']) 157 | 158 | crowding['total_entries_since_last_train'] = crowding.groupby(['STATION','LINENAME']).total_entries.diff().replace(to_replace=0,value=np.nan) 159 | crowding['total_exits_since_last_train'] = crowding.groupby(['STATION','LINENAME']).total_exits.diff().replace(to_replace=0,value=np.nan) 160 | crowding['total_exits_before_next_train'] = crowding.groupby(['STATION','LINENAME']).total_exits.diff(-1).replace(to_replace=0,value=np.nan) 161 | 162 | crowding['total_entries_since_last_train'] = crowding.groupby(['STATION','LINENAME']).total_entries_since_last_train.apply(lambda group: group.interpolate(method='ffill')).fillna(0).round() 163 | crowding['total_exits_since_last_train'] = crowding.groupby(['STATION','LINENAME']).total_exits_since_last_train.apply(lambda group: group.interpolate(method='ffill')).fillna(0).round() 164 | crowding['total_exits_before_next_train'] = crowding.groupby(['STATION','LINENAME']).total_exits_before_next_train.apply(lambda group: group.interpolate(method='bfill')).fillna(0).round().abs() 165 | 166 | crowding.loc[crowding.total_entries_since_last_train <0,'total_entries_since_last_train'] = 0 167 | crowding.loc[crowding.total_exits_since_last_train <0,'total_exits_since_last_train'] = 0 168 | crowding.loc[crowding.total_exits_before_next_train <0,'total_exits_before_next_train'] = 0 169 | 170 | crowding.loc[crowding.total_entries_since_last_train >1000,'total_entries_since_last_train'] = 0 171 | crowding.loc[crowding.total_exits_since_last_train > 1000,'total_exits_since_last_train'] = 0 172 | crowding.loc[crowding.total_exits_before_next_train > 1000,'total_exits_before_next_train'] = 0 173 | 174 | crowding = crowding.drop_duplicates() 175 | 176 | logging.getLogger().info("Getting people waiting at stations") 177 | crowding = crowding.groupby(['STATION','LINENAME']).apply(get_people_waiting) 178 | 179 | logging.getLogger().info("Getting crowd per train") 180 | crowding = crowding.groupby('uid').apply(get_train_crowd, entry_exit_ratio=entry_exit_ratio) 181 | 182 | logging.getLogger().info("Finishing") 183 | return crowding 184 | 185 | 186 | def fill_missing_hours(df): 187 | tmp = df.set_index('hour').reindex(range(0, 24)).reset_index() 188 | tmp = tmp.fillna(0) 189 | return tmp 190 | 191 | 192 | def get_hourly_averages(crowd_by_station_line, clean_stop_routes): 193 | crowd_by_station_line = crowd_by_station_line.groupby(['STATION','route_id','direction_id','time','gtfs_stop_id']).mean()['crowd'].reset_index() 194 | crowd_by_station_line['route_stop'] = crowd_by_station_line['route_id'] + '_' + crowd_by_station_line['gtfs_stop_id'] 195 | 196 | crowd_by_station_line = crowd_by_station_line[crowd_by_station_line.route_stop.isin(clean_stop_routes.route_stop)] 197 | 198 | avg_estimates = crowd_by_station_line.copy() 199 | avg_estimates['day_of_week'] = avg_estimates.time.dt.weekday 200 | avg_estimates['tmp_time'] = avg_estimates.time + timedelta(minutes=30) 201 | avg_estimates['hour'] = avg_estimates.tmp_time.dt.hour 202 | avg_estimates.crowd = avg_estimates.crowd.fillna(0) 203 | avg_estimates.crowd = [np.ceil(x) for x in avg_estimates.crowd] 204 | 205 | 206 | weekday_avg_estimates = avg_estimates[avg_estimates.day_of_week < 5].copy() 207 | weekday_avg_estimates = weekday_avg_estimates.groupby(['STATION','route_id','hour']).mean()['crowd'].reset_index() 208 | 209 | avg_estimates_filled = weekday_avg_estimates.groupby(['STATION','route_id']).apply(fill_missing_hours) 210 | avg_estimates_filled.drop(columns=['STATION','route_id'],inplace=True) 211 | avg_estimates_filled.reset_index(inplace=True) 212 | avg_estimates_filled = avg_estimates_filled.groupby(['STATION','route_id','hour']).mean()['crowd'].reset_index() 213 | avg_estimates_filled.crowd = np.round(avg_estimates_filled.crowd) 214 | 215 | avg_estimates['weekday'] = [1 if x < 5 else 0 for x in avg_estimates.day_of_week] 216 | avg_estimates = avg_estimates.groupby(['STATION','route_id','hour','weekday','direction_id']).mean()['crowd'].reset_index() 217 | 218 | avg_estimates_split = avg_estimates.groupby(['STATION','route_id','direction_id','weekday']).apply(fill_missing_hours) 219 | # avg_estimates_split.head() 220 | avg_estimates_split.drop(columns=['STATION','route_id','weekday','direction_id'],inplace=True) 221 | avg_estimates_split.reset_index(inplace=True) 222 | avg_estimates_split = avg_estimates_split.groupby(['STATION','route_id','hour','direction_id','weekday']).mean()['crowd'].reset_index() 223 | avg_estimates_split.crowd = np.round(avg_estimates_split.crowd) 224 | 225 | return avg_estimates_filled, avg_estimates_split -------------------------------------------------------------------------------- /scripts/make_stops_for_line.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import networkx as nx 3 | 4 | graph = nx.Graph() 5 | 6 | station_to_station = pd.read_csv('./station_to_station.csv') 7 | 8 | def generate_order(group,stop_lookup): 9 | graph = nx.DiGraph() 10 | for index, row in group.iterrows(): 11 | graph.add_edge(row['from'],row['to']) 12 | ordered_stops= pd.DataFrame([ (node,stop_lookup[node], index) for index,node in enumerate(graph.nodes())], columns=['stop_id','stop_name', 'order']) 13 | return ordered_stops 14 | 15 | 16 | south_bound = station_to_station[station_to_station['from'].str[-1]=='S'] 17 | 18 | stops = pd.read_csv('./stops.txt') 19 | stop_lookup = dict(zip(stops.stop_id, stops.stop_name)) 20 | 21 | 22 | all_ordered = pd.DataFrame() 23 | for line, data in south_bound.groupby('line'): 24 | ordered = generate_order(data, stop_lookup) 25 | ordered = ordered.assign(line = line) 26 | all_ordered = all_ordered.append(ordered) 27 | 28 | 29 | name_id_lookup = pd.read_csv('./cleaned_crosswalk.csv') 30 | 31 | 32 | 33 | final_ordered_stations = pd.merge(all_ordered,name_id_lookup, left_on= ['stop_name','line'], right_on=['name','route_id']).drop('name',axis=1) 34 | final_ordered_stations.rename(columns={'stop_name':'station'}).to_csv('../public/stops.csv', index=False) 35 | # station_to_station.to_csv('../public/stops.csv', index=False) 36 | 37 | # station_to_station = station_to_station[station_to_station['from_station']!="none"] 38 | -------------------------------------------------------------------------------- /scripts/requirements.txt: -------------------------------------------------------------------------------- 1 | # This file may be used to create an environment using: 2 | # $ conda create --name --file 3 | # platform: osx-64 4 | appnope==0.1.0=py38h32f6830_1002 5 | argon2-cffi=20.1.0=py38h4d0b108_2 6 | async_generator=1.10=py_0 7 | attrs=20.2.0=pyh9f0ad1d_0 8 | backcall=0.2.0=pyh9f0ad1d_0 9 | backports=1.0=py_2 10 | backports.functools_lru_cache=1.6.1=py_0 11 | blas=1.0=mkl 12 | bleach>=3.3.0 13 | brotlipy=0.7.0=py38h94c058a_1001 14 | ca-certificates=2020.6.20=hecda079_0 15 | cachetools=4.1.1=pypi_0 16 | certifi=2020.6.20=py38h5347e94_2 17 | cffi=1.14.3=py38hed5b41f_0 18 | chardet=3.0.4=py38h5347e94_1008 19 | cryptography>=3.2 20 | cycler=0.10.0=pypi_0 21 | decorator=4.4.2=py_0 22 | defusedxml=0.6.0=py_0 23 | entrypoints=0.3=py38h32f6830_1002 24 | fastparquet=0.4.1=py38h9c4ca6c_0 25 | google-api-core=1.23.0=pypi_0 26 | google-auth=1.22.1=pypi_0 27 | google-cloud-core=1.4.3=pypi_0 28 | google-cloud-storage=1.32.0=pypi_0 29 | google-crc32c=1.0.0=pypi_0 30 | google-resumable-media=1.1.0=pypi_0 31 | googleapis-common-protos=1.52.0=pypi_0 32 | idna=2.10=pyh9f0ad1d_0 33 | importlib-metadata=2.0.0=py_1 34 | importlib_metadata=2.0.0=1 35 | intel-openmp=2019.4=233 36 | ipykernel=5.3.4=py38h1cdfbd6_1 37 | ipython=7.18.1=py38h1cdfbd6_1 38 | ipython_genutils=0.2.0=py_1 39 | jedi=0.17.2=py38h32f6830_1 40 | jinja2>=2.11.3 41 | json5=0.9.5=pyh9f0ad1d_0 42 | jsonschema=3.2.0=py_2 43 | jupyter_client=6.1.7=py_0 44 | jupyter_core=4.6.3=py38h32f6830_2 45 | jupyterlab=2.2.9=py_0 46 | jupyterlab_pygments=0.1.2=pyh9f0ad1d_0 47 | jupyterlab_server=1.2.0=py_0 48 | kiwisolver=1.2.0=pypi_0 49 | libcxx=10.0.0=1 50 | libedit=3.1.20191231=h1de35cc_1 51 | libffi=3.3=hb1e8313_2 52 | libllvm9=9.0.1=h7475705_1 53 | libsodium=1.0.17=h01d97ff_0 54 | llvmlite=0.33.0=py38h3707e27_1 55 | markupsafe=1.1.1=py38h94c058a_2 56 | matplotlib=3.3.2=pypi_0 57 | mistune=0.8.4=py38h4d0b108_1002 58 | mkl=2019.4=233 59 | mkl-service=2.3.0=py38hfbe908c_0 60 | mkl_fft=1.2.0=py38hc64f4ea_0 61 | mkl_random=1.1.1=py38h959d312_0 62 | nbclient=0.5.1=py_0 63 | nbconvert=6.0.7=py38h32f6830_1 64 | nbformat=5.0.8=py_0 65 | ncurses=6.2=h0a44026_1 66 | nest-asyncio=1.4.1=py_0 67 | notebook>=6.1.5 68 | numba=0.50.1=py38h9529b5f_1 69 | numpy=1.19.2=py38h456fd55_0 70 | numpy-base=1.19.2=py38hcfb5961_0 71 | openssl=1.1.1h=haf1e3a3_0 72 | packaging=20.4=pyh9f0ad1d_0 73 | pandas=1.1.3=py38hb1e8313_0 74 | pandoc=2.11.0.4=h22f3db7_0 75 | pandocfilters=1.4.2=py_1 76 | parso=0.7.1=pyh9f0ad1d_0 77 | pexpect=4.8.0=pyh9f0ad1d_2 78 | pickleshare=0.7.5=py_1003 79 | pillow>=8.1.1 80 | pip=20.2.4=py38_0 81 | prometheus_client=0.8.0=pyh9f0ad1d_0 82 | prompt-toolkit=3.0.8=py_0 83 | protobuf=3.13.0=pypi_0 84 | ptyprocess=0.6.0=py_1001 85 | pyarrow=2.0.0=pypi_0 86 | pyasn1=0.4.8=pypi_0 87 | pyasn1-modules=0.2.8=pypi_0 88 | pycparser=2.20=pyh9f0ad1d_2 89 | pygments>=2.7.4 90 | pyopenssl=19.1.0=py_1 91 | pyparsing=2.4.7=pyh9f0ad1d_0 92 | pyrsistent=0.17.3=py38h4d0b108_1 93 | pysocks=1.7.1=py38h5347e94_2 94 | python=3.8.5=h26836e1_1 95 | python-dateutil=2.8.1=py_0 96 | python_abi=3.8=1_cp38 97 | pytz=2020.1=py_0 98 | pyzmq=19.0.1=py38h1fcdcd6_0 99 | readline=8.0=h1de35cc_0 100 | requests=2.24.0=pyh9f0ad1d_0 101 | rsa=4.6=pypi_0 102 | scipy=1.5.3=pypi_0 103 | send2trash=1.5.0=py_0 104 | setuptools=50.3.0=py38h0dc7051_1 105 | six=1.15.0=py_0 106 | sqlite=3.33.0=hffcf06c_0 107 | terminado=0.9.1=py38h32f6830_1 108 | testpath=0.4.4=py_0 109 | thrift=0.11.0=py38h4a8c4bd_1001 110 | tk=8.6.10=hb0a8c7a_0 111 | tornado=6.0.4=py38h4d0b108_2 112 | traitlets=5.0.5=py_0 113 | urllib3=1.25.11=py_0 114 | wcwidth=0.2.5=pyh9f0ad1d_2 115 | webencodings=0.5.1=py_1 116 | wheel=0.35.1=py_0 117 | xz=5.2.5=h1de35cc_0 118 | zeromq=4.3.2=h6de7cb9_2 119 | zipp=3.4.0=py_0 120 | zlib=1.2.11=h1de35cc_3 121 | -------------------------------------------------------------------------------- /scripts/turnstile.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import sys, logging 4 | from datetime import datetime, timedelta 5 | import matplotlib.pyplot as plt 6 | import re 7 | import os 8 | import bisect 9 | import io 10 | import logging 11 | import re 12 | import requests 13 | 14 | from ast import literal_eval 15 | from datetime import datetime, timedelta 16 | from html.parser import HTMLParser 17 | from typing import List, Dict 18 | 19 | # This module provides methods that handles MTA turnstile data 20 | 21 | 22 | def _process_raw_data(raw_data: pd.DataFrame, group_by: List[str]) -> pd.DataFrame: 23 | logging.getLogger().info("Cleaning turnstile data") 24 | 25 | # create datetime from DATE and TIME columns 26 | processed = raw_data.assign( 27 | datetime=pd.to_datetime( 28 | raw_data['DATE'] + " " + raw_data['TIME'], 29 | format="%m/%d/%Y %H:%M:%S")) 30 | 31 | # remove mysterious duplicate index along STATION + UNIT 32 | processed = processed.groupby( 33 | group_by + ['datetime']).sum().reset_index() 34 | 35 | processed = processed.set_index(pd.DatetimeIndex(processed.datetime)) 36 | processed.drop(columns=['datetime'], inplace=True) 37 | 38 | # clean up whitespace in the columns 39 | processed.rename(columns={c: c.strip() 40 | for c in processed.columns}, inplace=True) 41 | 42 | return processed 43 | 44 | 45 | def _process_grouped_data(grouped: pd.DataFrame, 46 | frequency: str) -> pd.DataFrame: 47 | # calculate the diff and take the absolute value 48 | entry_diffs = grouped.ENTRIES.diff() 49 | exit_diffs = grouped.EXITS.diff() 50 | 51 | # clean up data 52 | # grouped.loc[entry_diffs < 0, 'entry_diffs'] = np.nan 53 | # grouped.loc[exit_diffs < 0, 'exit_diffs'] = np.nan 54 | # grouped.loc[entry_diffs > 10000, 'entry_diffs'] = np.nan 55 | # grouped.loc[exit_diffs > 10000, 'exit_diffs'] = np.nan 56 | 57 | entry_diffs = pd.Series([np.nan if (x < 0)|(x>10000) else x for x in entry_diffs]) 58 | exit_diffs = pd.Series([np.nan if (x < 0)|(x>10000) else x for x in exit_diffs]) 59 | 60 | # restore cumulative data 61 | cleaned_entries = entry_diffs.cumsum() 62 | cleaned_exits = exit_diffs.cumsum() 63 | 64 | # assign new columns 65 | grouped = grouped.assign( 66 | entry_diffs=entry_diffs.values, 67 | exit_diffs=exit_diffs.values, 68 | cleaned_entries=cleaned_entries.values, 69 | cleaned_exits=cleaned_exits.values, 70 | ) 71 | 72 | resampled = grouped.resample(frequency).asfreq() 73 | interpolated_group = pd.concat([resampled, grouped]) 74 | interpolated_group = interpolated_group.loc[~interpolated_group.index.duplicated( 75 | keep='first')] 76 | interpolated_group = interpolated_group.sort_index(ascending=True) 77 | if interpolated_group[interpolated_group.cleaned_entries.notnull()].shape[0] > 2: 78 | interpolated_group.cleaned_entries.interpolate( 79 | method='quadratic', inplace=True) 80 | else: 81 | interpolated_group.cleaned_entries.interpolate( 82 | method='linear', inplace=True) 83 | 84 | if interpolated_group[interpolated_group.cleaned_exits.notnull()].shape[0] > 2: 85 | interpolated_group.cleaned_exits.interpolate(method='quadratic', inplace=True) 86 | else: 87 | interpolated_group.cleaned_exits.interpolate(method='linear', inplace=True) 88 | 89 | interpolated_group = interpolated_group.assign( 90 | estimated_entries=interpolated_group.cleaned_entries.diff(), 91 | estimated_exits=interpolated_group.cleaned_exits.diff()) 92 | interpolated_group.fillna(method='ffill', inplace=True) 93 | interpolated_group = interpolated_group.loc[resampled.index] 94 | interpolated_group.drop( 95 | columns=[ 96 | "ENTRIES", 97 | "EXITS", 98 | "cleaned_entries", 99 | "cleaned_exits"], 100 | inplace=True) 101 | return interpolated_group 102 | 103 | 104 | def _interpolate(intervalized_data: pd.DataFrame, 105 | group_by: List[str], 106 | frequency: str) -> pd.DataFrame: 107 | logging.getLogger().info("Start interpolating turnstile data") 108 | 109 | interpolated = [] 110 | intervalized_data.groupby(group_by).apply( 111 | lambda g: interpolated.append(_process_grouped_data(g, frequency))) 112 | logging.getLogger().info("Finish interpolating") 113 | result = pd.concat(interpolated) 114 | logging.getLogger().info("Finish concatenating the result") 115 | 116 | return result 117 | 118 | 119 | class TurnstilePageParser(HTMLParser): 120 | def __init__(self, start_date, end_date=None): 121 | super().__init__() 122 | self.start_date = start_date 123 | self.end_date = end_date 124 | self.href = False 125 | self.links = [] 126 | 127 | def handle_starttag(self, tag, attrs): 128 | if tag == "a": 129 | for name, value in attrs: 130 | if name == "href": 131 | self.href = True 132 | self.link = value 133 | 134 | def handle_endtag(self, tag): 135 | if tag == "a": 136 | self.href = False 137 | 138 | def handle_data(self, data): 139 | if self.href: 140 | try: 141 | d = datetime.strptime(data.strip(), '%A, %B %d, %Y') 142 | except ValueError: 143 | pass 144 | else: 145 | self.links.append((d, self.link)) 146 | 147 | def get_all_links(self): 148 | self.links.sort(key=lambda r: r[0]) 149 | keys = [r[0] for r in self.links] 150 | lower = bisect.bisect_left(keys, self.start_date) 151 | if lower != len(keys): 152 | lower = max( 153 | 0, lower - 1) if keys[lower] == self.start_date else lower 154 | else: 155 | lower = 0 156 | 157 | upper = len(keys) - 1 158 | if self.end_date: 159 | upper = bisect.bisect_right(keys, self.end_date) 160 | if upper != len(keys): 161 | upper = min(len(keys) - 1, upper + 162 | 1) if keys[upper] == self.end_date else upper 163 | else: 164 | upper = len(keys) - 1 165 | return [r[1] for r in self.links[lower:upper + 1]] 166 | 167 | 168 | def download_turnstile_data(start_date: datetime, 169 | end_date: datetime = None) -> pd.DataFrame: 170 | """ 171 | Download raw turnstile data from http://web.mta.info/developers/turnstile.html 172 | 173 | Parameters 174 | start_date: datatime 175 | end_date: datetime, optional 176 | 177 | Return 178 | pandas.DataFrame 179 | 180 | """ 181 | logging.getLogger().info("Downloading turnstile data") 182 | mta_link_rook = 'http://web.mta.info/developers/' 183 | start_page = requests.get(mta_link_rook + 'turnstile.html') 184 | parser = TurnstilePageParser(start_date, end_date) 185 | parser.feed(start_page.content.decode('utf-8')) 186 | dfs = [ 187 | pd.read_csv( 188 | io.StringIO( 189 | requests.get( 190 | mta_link_rook + 191 | l).content.decode('utf-8'))) for l in parser.get_all_links()] 192 | return pd.concat(dfs) 193 | 194 | 195 | def create_interpolated_turnstile_data( 196 | start_date: datetime, 197 | end_date: datetime = None, 198 | group_by: List[str] = ['UNIT', 'SCP'], 199 | frequency: str = '1H') -> pd.DataFrame: 200 | """ 201 | Create interpolated turnstile data 202 | 203 | Raw turnstile data is downloaded from http://web.mta.info/developers/turnstile.html 204 | For each turnstile unit, the differences of ENTRIES/EXITS are taken between two snapshots 205 | and large difference (>= 10000) and negative values are set to zero. 206 | The cleaned data is linearly interpolated using the frequency provided 207 | 208 | Parameters 209 | start_date : datetime 210 | end_date : datetime, optional 211 | group_by : List(str), optional 212 | frequency: str, optional 213 | 214 | Returns 215 | dataframe 216 | [group_by_keys: List[str] 217 | estimated_entries: int 218 | estimated_exits: int] 219 | 220 | """ 221 | 222 | if not set(group_by).issubset(['STATION', 'LINENAME', 'UNIT', 'SCP']): 223 | raise Exception("Unsupported group by keys: " + str(group_by)) 224 | 225 | 226 | raw = download_turnstile_data(start_date, end_date) 227 | raw['date'] = pd.to_datetime(raw.DATE) 228 | raw = raw[(raw.date <= (end_date + timedelta(1))) & (raw.date >= (start_date - timedelta(1)))] 229 | raw.drop('date',axis=1,inplace=True) 230 | 231 | interpolated = _interpolate(_process_raw_data(raw, group_by), group_by, frequency) 232 | end_date = end_date or interpolated.index.max() 233 | return interpolated[interpolated.index.to_series().between( 234 | start_date, end_date)] .drop(columns=["entry_diffs", "exit_diffs"]) 235 | 236 | 237 | def aggregate_turnstile_data_by_station(turnstile_data: pd.DataFrame, 238 | output_directory: str = None) -> Dict[str, 239 | pd.DataFrame]: 240 | """ 241 | aggregate turnstile data by station and save to output directory if passed. 242 | 243 | Parameters 244 | turnstile_data: pandas.DataFram 245 | output_directory: str, optional - If specified, the data by station will be saved under the specified directory. 246 | 247 | 248 | Return 249 | dict[station_name:str, station_turnstile_data: pd.DataFrame] will be returned. 250 | 251 | """ 252 | 253 | aggregated_by_station = turnstile_data.groupby( 254 | ['datetime', 'STATION','LINENAME']).sum().reset_index() 255 | turnstile_by_station = { 256 | re.sub( 257 | r"\s+", 258 | '_', 259 | re.sub( 260 | r"[/|-]", 261 | " ", 262 | '_'.join(station))) + 263 | ".csv": df for ( 264 | station, 265 | df) in aggregated_by_station.groupby( 266 | ['STATION','LINENAME'])} 267 | if output_directory: 268 | if not os.path.exists(output_directory): 269 | os.mkdir(output_directory) 270 | for key in turnstile_by_station: 271 | d = turnstile_by_station[key] 272 | d.to_csv( 273 | os.path.join( 274 | output_directory.strip('/'), 275 | '/') + key, 276 | index=False) 277 | return turnstile_by_station 278 | 279 | 280 | def pre_interpolation_fix(turnstile_data_raw, pct_6=0.1, pct_9=0.4): 281 | ''' 282 | Pre-interpolation adjustments for turnstile data for better interpolated estimates during rush-hour 283 | - Imputing values for 6am to be 10% of the entries/exits between 4 and 8am 284 | - Imputing values for 9am to be 40% of the entries/exits between 8am and 12pm 285 | ''' 286 | turnstile_data_raw = turnstile_data_raw.reset_index() 287 | turnstile_data_raw = turnstile_data_raw.sort_values('datetime') 288 | turnstile_resampled = turnstile_data_raw.copy() 289 | 290 | turnstile_data_raw['entry_diff'] = turnstile_data_raw.groupby(['STATION','LINENAME','UNIT']).ENTRIES.diff() 291 | turnstile_data_raw['exit_diff'] = turnstile_data_raw.groupby(['STATION','LINENAME','UNIT']).EXITS.diff() 292 | turnstile_data_raw = turnstile_data_raw[(turnstile_data_raw.entry_diff >= 0)&(turnstile_data_raw.entry_diff < 10000)&(turnstile_data_raw.entry_diff >= -10000)] 293 | turnstile_data_raw = turnstile_data_raw.drop(columns=['entry_diff','exit_diff']) 294 | 295 | turnstile_resampled['entry_diff'] = turnstile_resampled.groupby(['STATION','LINENAME','UNIT']).ENTRIES.diff() 296 | turnstile_resampled['exit_diff'] = turnstile_resampled.groupby(['STATION','LINENAME','UNIT']).EXITS.diff() 297 | turnstile_resampled = turnstile_resampled[(turnstile_resampled.entry_diff >= 0)&(turnstile_resampled.entry_diff < 10000)&(turnstile_resampled.entry_diff >= -10000)] 298 | 299 | 300 | turnstile_resampled = turnstile_resampled.set_index('datetime').groupby(['STATION','LINENAME','UNIT']).resample('1H').asfreq().drop(columns=['STATION','LINENAME','UNIT']).reset_index() 301 | turnstile_resampled['hour'] = turnstile_resampled.datetime.dt.hour 302 | turnstile_resampled['weekday'] = turnstile_resampled.datetime.dt.weekday 303 | turnstile_resampled['ENTRIES'] = turnstile_resampled.groupby(['STATION','LINENAME','UNIT']).ENTRIES.apply(lambda group: group.interpolate(method='ffill')) 304 | turnstile_resampled['EXITS'] = turnstile_resampled.groupby(['STATION','LINENAME']).EXITS.apply(lambda group: group.interpolate(method='ffill')) 305 | turnstile_resampled['tot_entries'] = turnstile_resampled.groupby(['STATION','LINENAME','UNIT']).entry_diff.apply(lambda group: group.interpolate(method='bfill')) 306 | turnstile_resampled['tot_exits'] = turnstile_resampled.groupby(['STATION','LINENAME']).exit_diff.apply(lambda group: group.interpolate(method='bfill')) 307 | turnstile_resampled = turnstile_resampled[(turnstile_resampled.hour.isin([6,9]))&(turnstile_resampled.entry_diff.isna())&(turnstile_resampled.ENTRIES.notnull())&(turnstile_resampled.tot_entries.notnull())] 308 | turnstile_resampled = turnstile_resampled.drop(columns=['entry_diff','exit_diff']) 309 | 310 | turnstile_resampled['ENTRIES'] = [e + int(t*pct_6) if h == 6 else e + int(t*pct_9) for e,t,h in zip(turnstile_resampled['ENTRIES'],turnstile_resampled['tot_entries'], turnstile_resampled['hour'])] 311 | turnstile_resampled['EXITS'] = [e + int(t*pct_6) if h == 6 else e + int(t*pct_9) for e,t,h in zip(turnstile_resampled['EXITS'],turnstile_resampled['tot_exits'], turnstile_resampled['hour'])] 312 | 313 | turnstile_data_raw_imputed = pd.concat([turnstile_data_raw,turnstile_resampled.drop(columns=['weekday','hour','tot_entries','tot_exits'])],sort=False) 314 | turnstile_data_raw_imputed = turnstile_data_raw_imputed.sort_values('datetime') 315 | return turnstile_data_raw_imputed 316 | 317 | def consolidate_turnstile_data(turnstile_data): 318 | ''' 319 | Consolidates turnstile data across multiple units within a station complex 320 | ''' 321 | turnstile_data = turnstile_data.reset_index() 322 | turnstile_data.loc[turnstile_data.estimated_entries < 0, 'estimated_entries'] = 0 323 | turnstile_data.loc[turnstile_data.estimated_exits < 0, 'estimated_exits'] = 0 324 | turnstile_data = turnstile_data.groupby(['STATION','LINENAME','datetime']).sum().reset_index() 325 | turnstile_data['total_entries'] = turnstile_data.groupby(['STATION','LINENAME']).estimated_entries.cumsum() 326 | turnstile_data['total_exits'] = turnstile_data.groupby(['STATION','LINENAME']).estimated_exits.cumsum() 327 | 328 | stations_with_multiple_line_patterns = ['TIMES SQ-42 ST', '59 ST', '14 ST-UNION SQ', '161/YANKEE STAD', '168 ST', '34 ST-PENN STA', '42 ST-PORT AUTH', '59 ST COLUMBUS', 'ATL AV-BARCLAY', 'BOROUGH HALL', 'FULTON ST'] 329 | line_patterns = ['1237ACENQRSW', 'NQR456W', '456LNQRW', '4BD', '1AC', '123ACE', 'ACENQRS1237W', '1ABCD', '2345BDNQR', '2345R', '2345ACJZ'] 330 | modified_linename = pd.DataFrame({'STATION':stations_with_multiple_line_patterns,'lines':line_patterns}) 331 | 332 | turnstile_to_consolidate = turnstile_data[(turnstile_data.STATION.isin(stations_with_multiple_line_patterns))] 333 | turnstile_to_consolidate = turnstile_to_consolidate[~(turnstile_to_consolidate.LINENAME == 'G')] 334 | 335 | turnstile_consolidated = turnstile_to_consolidate.groupby(['datetime','STATION']).sum().reset_index() 336 | 337 | turnstile_consolidated = turnstile_to_consolidate.merge(turnstile_consolidated,how='left',on=['datetime','STATION']).drop(columns=['estimated_entries_x','estimated_exits_x','total_entries_x','total_exits_x']).rename(columns={'estimated_entries_y':'estimated_entries','estimated_exits_y':'estimated_exits','total_entries_y':'total_entries','total_exits_y':'total_exits'}) 338 | turnstile_consolidated = turnstile_consolidated.merge(modified_linename,how='left',on='STATION').rename(columns={'lines':'modified_linename'}) 339 | turnstile_data_cleaned = turnstile_data[~turnstile_data.STATION.isin(stations_with_multiple_line_patterns)].copy() 340 | fulton_g = turnstile_data[(turnstile_data.STATION == 'FULTON ST')&(turnstile_data.LINENAME == 'G')].copy() 341 | turnstile_data_cleaned = turnstile_data_cleaned.append(fulton_g,sort=False) 342 | turnstile_data_cleaned['modified_linename'] = turnstile_data_cleaned.LINENAME 343 | turnstile_data_cleaned = turnstile_data_cleaned.append(turnstile_consolidated,sort=False) 344 | 345 | return turnstile_data_cleaned -------------------------------------------------------------------------------- /src/App.scss: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | width: 100vw; 4 | height: 100vh; 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: center; 8 | align-items: center; 9 | font-family: lato; 10 | box-sizing: border-box; 11 | padding: 20px 20px 0px 20px; 12 | display: flex; 13 | } 14 | 15 | .app-inner { 16 | display: flex; 17 | flex-direction: column; 18 | width: 100%; 19 | height: 100%; 20 | justify-content: center; 21 | 22 | @media only screen and (min-width: 900px) { 23 | max-width: 1024px; 24 | max-height: 900px; 25 | } 26 | } 27 | 28 | .explainer-text { 29 | font-size: 0.5em; 30 | color: grey; 31 | @media only screen and (min-width: 900px) { 32 | font-size: 0.7rem; 33 | } 34 | } 35 | 36 | .header { 37 | display: flex; 38 | flex-direction: column; 39 | align-items: center; 40 | justify-content: center; 41 | flex: 1; 42 | &.header-prompt-complete { 43 | flex: none; 44 | } 45 | } 46 | .hide-small { 47 | display: none; 48 | } 49 | 50 | @media only screen and (min-width: 900px) { 51 | .show-small { 52 | display: none; 53 | } 54 | .hide-small { 55 | display: block; 56 | } 57 | } 58 | 59 | .prompt-incomplete { 60 | .show-small { 61 | display: none; 62 | } 63 | .hide-small { 64 | display: block; 65 | } 66 | } 67 | 68 | .prompt-incomplete > div { 69 | margin-bottom: 20px; 70 | } 71 | 72 | .line-select { 73 | flex-direction: row; 74 | display: flex; 75 | } 76 | 77 | .App-logo { 78 | height: 40vmin; 79 | pointer-events: none; 80 | } 81 | 82 | .prompt { 83 | display: flex; 84 | align-items: center; 85 | justify-content: normal; 86 | flex-direction: column; 87 | } 88 | 89 | .prompt-complete { 90 | flex-direction: row; 91 | } 92 | 93 | h2 { 94 | font-weight: normal; 95 | font-size: 0.8rem; 96 | padding: 0px; 97 | @media only screen and (min-width: 900px) { 98 | padding: 13px; 99 | font-size: 1rem; 100 | } 101 | } 102 | 103 | .App-header { 104 | background-color: #282c34; 105 | min-height: 100vh; 106 | display: flex; 107 | flex-direction: column; 108 | align-items: center; 109 | justify-content: center; 110 | font-size: calc(10px + 2vmin); 111 | color: white; 112 | } 113 | 114 | .line-specification { 115 | display: flex; 116 | flex-direction: row; 117 | align-items: center; 118 | } 119 | 120 | .App-link { 121 | color: #61dafb; 122 | } 123 | 124 | @keyframes App-logo-spin { 125 | from { 126 | transform: rotate(0deg); 127 | } 128 | to { 129 | transform: rotate(360deg); 130 | } 131 | } 132 | 133 | .fade-in { 134 | opacity: 1; 135 | animation-name: fadeInOpacity; 136 | animation-iteration-count: 1; 137 | animation-timing-function: ease-in; 138 | animation-duration: 1s; 139 | } 140 | 141 | @keyframes fadeInOpacity { 142 | 0% { 143 | opacity: 0; 144 | } 145 | 100% { 146 | opacity: 1; 147 | } 148 | } 149 | 150 | footer { 151 | display: flex; 152 | flex-direction: column; 153 | justify-content: space-between; 154 | align-items: center; 155 | width: 100%; 156 | margin-top: 10px; 157 | .info-share, 158 | .disclaimer { 159 | display: flex; 160 | flex-direction: row; 161 | justify-content: space-between; 162 | align-items: center; 163 | width: 100%; 164 | } 165 | .info, 166 | .share-buttons { 167 | > * { 168 | margin-right: 10px; 169 | } 170 | } 171 | 172 | .disclaimer { 173 | justify-content: center; 174 | box-sizing: border-box; 175 | padding: 5px; 176 | > a, 177 | span { 178 | margin-right: 20px; 179 | color: grey; 180 | font-size: 0.5em; 181 | @media only screen and (min-width: 900px) { 182 | font-size: 0.7rem; 183 | } 184 | text-decoration: none; 185 | } 186 | } 187 | } 188 | 189 | .date-range-text { 190 | font-size: 0.8em; 191 | padding-left: 10px; 192 | padding-right: 10px; 193 | padding-top: 5px; 194 | padding-bottom: 5px; 195 | @media only screen and (min-width: 900px) { 196 | padding: 13px; 197 | font-size: 1rem; 198 | } 199 | } 200 | 201 | .graph { 202 | display: flex; 203 | flex-direction: column; 204 | width: 100%; 205 | overflow-y: auto; 206 | box-sizing: border-box; 207 | padding: 0px 20px 0px 20px; 208 | display: flex; 209 | flex-direction: column; 210 | flex: 1; 211 | h2 { 212 | margin-bottom: 0px; 213 | margin-top: 0px; 214 | } 215 | } 216 | 217 | .graph > div { 218 | width: 100%; 219 | // height:50%; 220 | box-sizing: border-box; 221 | } 222 | 223 | .hourly-chart { 224 | position: relative; 225 | display: flex; 226 | flex-direction: column; 227 | flex: 1; 228 | padding: 0px; 229 | margin: 0px; 230 | max-height: 30vh; 231 | @media only screen and (max-width: 480px) { 232 | max-height: none; 233 | } 234 | @media only screen and (min-width: 900px) { 235 | padding: 0px 20px 0px 20px; 236 | max-height: none; 237 | } 238 | } 239 | 240 | @media only screen and (min-width: 900px) { 241 | .graph { 242 | flex-direction: row; 243 | } 244 | .graph > div { 245 | height: 100%; 246 | width: 50%; 247 | } 248 | 249 | .prompt { 250 | flex-direction: row; 251 | } 252 | } 253 | 254 | .info { 255 | display: flex; 256 | flex-direction: row; 257 | align-items: center; 258 | justify-content: space-between; 259 | } 260 | 261 | .share-buttons { 262 | display: flex; 263 | flex-direction: row; 264 | align-items: center; 265 | } 266 | 267 | .stops-chart-container { 268 | display: flex; 269 | flex-direction: column; 270 | overflow: hidden; 271 | flex: 1; 272 | @media only screen and (min-width: 900px) { 273 | padding: 0px 20px; 274 | } 275 | } 276 | 277 | #root { 278 | width: 100%; 279 | height: 100%; 280 | display: flex; 281 | flex-direction: column; 282 | align-items: center; 283 | justify-content: center; 284 | } 285 | 286 | .info-button { 287 | cursor: pointer; 288 | } 289 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/AppSyles.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const ControlBar = styled.div` 4 | display: flex; 5 | flex-direction: row; 6 | justify-content: center; 7 | align-items: center; 8 | width: 100%; 9 | height: 30px; 10 | .reset-button { 11 | justify-self: center; 12 | } 13 | .day-direction { 14 | display: flex; 15 | flex-direction: row; 16 | align-items: center; 17 | } 18 | `; 19 | 20 | const ResetButton = styled.button` 21 | color: #27a3aa; 22 | background-color: white; 23 | border: none; // solid 2px #27a3aa; 24 | 25 | font-weight: bold; 26 | padding: 0px 5px 0px 5px; 27 | cursor: pointer; 28 | margin-top: 0px; 29 | `; 30 | export const Styles = { 31 | ControlBar, 32 | ResetButton, 33 | }; 34 | -------------------------------------------------------------------------------- /src/Contexts/DataContext.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import Papa from 'papaparse' 3 | import {Station,Line, Stop, CrowdingObservation, RawCrowding, Direction, CarsByLine} from '../types' 4 | 5 | type Data={ 6 | stations: Station[] | null, 7 | lines: Line[] | null, 8 | stops: Stop[] | null, 9 | crowdingData:CrowdingObservation[] | null, 10 | dataLoaded : boolean, 11 | carsByLine: CarsByLine[] | null, 12 | dateRange: string | null, 13 | } 14 | export const DataContext = React.createContext({stations:null, lines:null,stops:null, crowdingData:null,dataLoaded:false, carsByLine:null, dateRange:null}); 15 | 16 | export const DataProvider :React.FC = ({children})=>{ 17 | const [stations, setStations] = useState(null) 18 | const [lines, setLines] = useState(null) 19 | const [stops, setStops] = useState(null) 20 | const [dataLoaded,setDataLoaded] = useState(false) 21 | const [crowdingData,setCrowdingData] = useState(null); 22 | const [carsByLine, setCarsByLine] = useState(null); 23 | const [dateRange, setDateRange] = useState(null); 24 | 25 | useEffect( ()=>{ 26 | loadCrowdingData().then( (data:any)=>{ 27 | setCrowdingData(data) 28 | }) 29 | },[]) 30 | 31 | useEffect(() => { 32 | loadCarsByLine().then((data:any) => { 33 | setCarsByLine(data) 34 | }) 35 | }, []) 36 | 37 | useEffect(()=>{ 38 | loadStops().then( (data : any) =>{ 39 | setStations(data.stations) 40 | setLines(data.lines) 41 | setStops(data.stops) 42 | setDataLoaded(true) 43 | }) 44 | },[]) 45 | 46 | useEffect(() => { 47 | fetch("timestamp.txt") 48 | .then(response => response.text()) 49 | .then(data => { 50 | const dates = data.split("-", 2); 51 | const beginDate = new Date(parseInt(dates[0].slice(0, 4)), parseInt(dates[0].slice(4, 6))-1, parseInt(dates[0].slice(6, 8))); 52 | const beginDateString = `${beginDate.getMonth()+1}-${beginDate.getDate()}-${beginDate.getFullYear()}`; 53 | const endDate = new Date(parseInt(dates[1].slice(0, 4)), parseInt(dates[1].slice(4, 6))-1, parseInt(dates[1].slice(6, 8))); 54 | const endDateString = `${endDate.getMonth()+1}-${endDate.getDate()}-${endDate.getFullYear()}`; 55 | const dateRangeString = `${beginDateString} - ${endDateString}`; 56 | setDateRange(dateRangeString); 57 | }) 58 | }) 59 | 60 | return( 61 | 62 | {children} 63 | 64 | ) 65 | } 66 | 67 | const iconForLine = (line:string)=>( 68 | `https://raw.githubusercontent.com/louh/mta-subway-bullets/master/svg/${line.toLocaleLowerCase()}.svg` 69 | ) 70 | 71 | function parseStops(stops : Stop[]){ 72 | const lineNames = Array.from(new Set(stops.map(stop=>stop.line))).filter(l=>l) 73 | const stationIds = Array.from(new Set(stops.map(stop=>stop.id))).filter(l=>l) 74 | const lines = lineNames.map(line=> ({ 75 | name:line, 76 | id: line, 77 | icon:iconForLine(line) 78 | })) 79 | 80 | const stations : Station[] =stationIds.map(stationId=>{ 81 | const station = stops.find(stop=>stop.id === stationId) 82 | if(!station){ 83 | throw("Something weird happened") 84 | } 85 | return { 86 | name: station.station, 87 | turnstile_name: station.station, 88 | id: stationId, 89 | lines: stops.filter(stop=>stop.id === stationId).map(stop => stop.line) 90 | } 91 | }) 92 | 93 | return {stations, lines, stops} 94 | } 95 | 96 | function parseCrowding(rawObservations: RawCrowding[]) : CrowdingObservation[]{ 97 | return rawObservations.map((observation: RawCrowding)=>({ 98 | stationID: observation.STATION, 99 | lineID: observation.route_id, 100 | hour:observation.hour, 101 | numPeople:observation.current_crowd, 102 | numPeopleLastMonth: observation.last_month_crowd, 103 | numPeopleLastYear: observation.last_year_crowd, 104 | direction: observation.direction_id === 0 ? Direction.NORTHBOUND : Direction.SOUTHBOUND, 105 | weekday: observation.weekday === 1, 106 | })) 107 | } 108 | 109 | function loadStops (){ 110 | return new Promise((resolve,reject)=>{ 111 | Papa.parse('/stops.csv',{ 112 | download:true, 113 | complete: (result :any)=> resolve(parseStops(result.data)), 114 | header:true, 115 | dynamicTyping: {order: true} 116 | 117 | }) 118 | }) 119 | } 120 | 121 | function loadCrowdingData(){ 122 | return new Promise((resolve,reject)=>{ 123 | Papa.parse('crowding_by_weekday_direction.csv',{ 124 | download:true, 125 | complete: (data :any)=> resolve(parseCrowding(data.data)), 126 | header:true, 127 | dynamicTyping: {hour: true, current_crowd: true,last_month_crowd:true, last_year_crowd:true, weekday:true, direction_id:true, } 128 | }) 129 | }) 130 | } 131 | 132 | function loadCarsByLine() { 133 | return new Promise((resolve, reject) => { 134 | Papa.parse('cars_by_line.csv', { 135 | download: true, 136 | complete: (result: any) => resolve(result.data), 137 | header: true, 138 | dynamicTyping: {num_cars: true} 139 | }) 140 | }) 141 | } 142 | -------------------------------------------------------------------------------- /src/Hooks/useCrowdingData.ts: -------------------------------------------------------------------------------- 1 | import {useState,useEffect, useContext} from 'react' 2 | import {CrowdingObservation,HourlyObservation, Stop, Direction, CarsByLine} from '../types' 3 | import {filerTruthy} from '../utils' 4 | import {DataContext} from '../Contexts/DataContext' 5 | 6 | export const useCrowdingData = (stationID :string |null, lineID:string | null)=>{ 7 | const {crowdingData} = useContext(DataContext) 8 | const [stationData, setStationData] = useState(null) 9 | 10 | useEffect( ()=> { 11 | if(crowdingData && lineID && stationID){ 12 | const data = crowdingData?.filter((observation)=> observation.stationID === stationID && observation.lineID === lineID) 13 | setStationData(data) 14 | } 15 | else{ 16 | setStationData(null) 17 | } 18 | },[crowdingData, lineID, stationID]) 19 | 20 | return stationData 21 | } 22 | 23 | 24 | export const useAbsoluteMaxForStops = (stops:Stop[] | null)=>{ 25 | const {crowdingData, carsByLine} = useContext(DataContext) 26 | if(stops && crowdingData && carsByLine){ 27 | 28 | const stationIDS = stops.map(s=>s.id) 29 | const lines = stops.map(s=>s.line) 30 | 31 | // const stopCounts = crowdingData.filter(cd=> stationIDS.includes(cd.stationID) && lines.includes(cd.lineID)) 32 | // const stopCounts = stops.map(stop=> crowdingData.find(s=> stop.id === s.stationID && stop.line === s.lineID)) 33 | const median = (arr: number[]) => { 34 | const mid = Math.floor(arr.length / 2), nums = [...arr].sort((a, b) => a - b); 35 | return arr.length % 2 !== 0 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2; 36 | } 37 | const stopCounts = crowdingData.filter(cd => stationIDS.includes(cd.stationID) && lines.includes(cd.lineID)) 38 | // Use median number of cars instead of min or max to get the most reasonable absolute max per car 39 | const medianNumCars = median([...carsByLine.map(x => x ? x.num_cars : 1)]) 40 | const absoluteMaxCurentPerCar = (stopCounts ? Math.max(...stopCounts.map(cd=> cd ? cd.numPeople : 0)) : 0) / medianNumCars; 41 | const absoluteMaxMonthPerCar = (stopCounts ? Math.max(...stopCounts.map(cd=>cd ? cd.numPeopleLastMonth : 0)) : 0) / medianNumCars; 42 | const absoluteMaxYearPerCar = (stopCounts ? Math.max(...stopCounts.map(cd=>cd ? cd.numPeopleLastYear :0 )) : 0) / medianNumCars; 43 | return { 44 | current: absoluteMaxCurentPerCar, 45 | month: absoluteMaxMonthPerCar, 46 | year: absoluteMaxYearPerCar 47 | } 48 | } 49 | else{ 50 | return null 51 | } 52 | } 53 | 54 | const getCarsForLine = (carsByLine : CarsByLine[] | null, lineID:string | null | undefined):number => { 55 | if(carsByLine && lineID){ 56 | const val = carsByLine?.find(x => lineID === x.line) 57 | if(val){ 58 | return val.num_cars 59 | } 60 | else{ 61 | return 0 62 | } 63 | } 64 | else{ 65 | return 0 66 | } 67 | } 68 | 69 | export const useMaxCrowdingByHourForTrip = (stops:Stop[] |null, order: Direction|null , weekday:boolean)=>{ 70 | const {crowdingData, carsByLine} = useContext(DataContext) 71 | const [data,setData] = useState(null) 72 | useEffect(()=>{ 73 | if(stops && crowdingData && (order!==null)){ 74 | const lines = stops.map(s=>s.line) 75 | const stations = stops.map(s=> s.id) 76 | const hourlyStopData = crowdingData.filter(cd => lines.includes(cd.lineID) && stations.includes(cd.stationID) && (cd.direction===order) && (cd.weekday === weekday )) 77 | const maxByHour: HourlyObservation[] = []; 78 | for(let hour =0; hour< 24; hour++){ 79 | const counts = hourlyStopData?.filter(obs=>(obs.hour===hour)).filter(filerTruthy) 80 | const countsCurrent = counts.map(obs => Math.round(obs.numPeople / getCarsForLine(carsByLine, obs.lineID) )) 81 | const countsLastMonth = counts.map(obs => Math.round(obs.numPeopleLastMonth / getCarsForLine(carsByLine, obs.lineID))) 82 | const countsLastYear = counts.map(obs => Math.round(obs.numPeopleLastYear / getCarsForLine(carsByLine, obs.lineID))) 83 | 84 | maxByHour.push({ 85 | hour:hour, 86 | numPeople: countsCurrent.length > 0 ? Math.max(...countsCurrent) : 0, 87 | numPeopleLastMonth: countsLastMonth.length > 0 ? Math.max(...countsLastMonth) : 0, 88 | numPeopleLastYear: countsLastYear.length > 0 ? Math.max(...countsLastYear) : 0 89 | }) 90 | } 91 | setData(maxByHour) 92 | } 93 | },[stops, crowdingData,weekday,order]) 94 | 95 | return data 96 | } 97 | 98 | export const useCrowdingDataByStops = (stops:Stop[] | null, hour:number | null, order: Direction|null, weekday:boolean)=>{ 99 | const {crowdingData, carsByLine} = useContext(DataContext) 100 | const [data, setData] = useState(null) 101 | 102 | useEffect(()=>{ 103 | if(stops && hour && crowdingData && carsByLine){ 104 | const stopCounts = stops.map(stop=> crowdingData.find(s=>s.hour=== hour && stop.id === s.stationID && stop.line === s.lineID && s.direction===order && s.weekday===weekday)).filter(filerTruthy) 105 | const stopCountsPerCar = stopCounts.map(stop => { 106 | const currentCountPerCar = Math.round( (stop?.numPeople ? stop.numPeople : 0 ) / getCarsForLine(carsByLine,stop?.lineID )) 107 | const lastMonthCountPerCar = Math.round( (stop?.numPeopleLastMonth ? stop.numPeopleLastMonth :0) / getCarsForLine(carsByLine,stop?.lineID )) 108 | const lastYearCountPerCar = Math.round( ( stop?.numPeopleLastYear ? stop?.numPeopleLastYear : 0 ) / getCarsForLine(carsByLine,stop?.lineID )) 109 | return { 110 | direction: stop?.direction, 111 | hour: stop?.hour, 112 | lineID: stop?.lineID, 113 | numPeople: currentCountPerCar, 114 | numPeopleLastMonth: lastMonthCountPerCar, 115 | numPeopleLastYear: lastYearCountPerCar, 116 | stationID: stop?.stationID, 117 | weekday: stop?.weekday 118 | } 119 | }) 120 | setData(stopCountsPerCar.filter(filerTruthy)) 121 | } 122 | },[stops,hour, crowdingData, weekday,order]) 123 | 124 | return data 125 | } 126 | 127 | -------------------------------------------------------------------------------- /src/Hooks/useStationsForLine.ts: -------------------------------------------------------------------------------- 1 | import {useContext} from 'react' 2 | import {DataContext} from '../Contexts/DataContext' 3 | import {filerTruthy} from '../utils' 4 | type RawStation={ 5 | station_name: string, 6 | clean_lines: string, 7 | turnstile_station_name:string, 8 | turnstile_lines: string 9 | } 10 | 11 | export const useStationsForLine = (line:string | null)=>{ 12 | const {stations, stops} = useContext(DataContext); 13 | const stationsInLine = stops?.filter(stop => stop.line==line ).sort((a,b)=> a.order > b.order ? 1 : -1) 14 | return stationsInLine?.map(stop => stations?.find(station => station.id === stop.id)).filter(filerTruthy) 15 | } -------------------------------------------------------------------------------- /src/Hooks/useStopsBetween.ts: -------------------------------------------------------------------------------- 1 | import {useState,useEffect, useContext} from 'react' 2 | import {DataContext} from '../Contexts/DataContext' 3 | import {Stop, Direction} from '../types' 4 | 5 | export const useStopsBetween = (line :string | null , start_station : string | null ,end_station: string | null)=>{ 6 | const [stopsBetween, setStopsBetween] = useState(null) 7 | const [order, setOrder] = useState(null) 8 | const {stops} = useContext(DataContext) 9 | 10 | useEffect(()=>{ 11 | if(start_station && end_station && stops){ 12 | const stops_for_line = stops.filter((stop)=> stop.line===line).sort((a,b)=> a.order > b.order ? 1: -1); 13 | const start_index = stops_for_line.findIndex(stop=> stop.id === start_station) 14 | const end_index = stops_for_line.findIndex(stop=> stop.id === end_station) 15 | 16 | const unordered_stops = stops_for_line.slice(Math.min(start_index,end_index), Math.max(start_index,end_index)+1) 17 | const order = start_index > end_index ? Direction.NORTHBOUND : Direction.SOUTHBOUND 18 | 19 | if(start_index > end_index){ 20 | unordered_stops.reverse() 21 | } 22 | setStopsBetween(unordered_stops); 23 | setOrder(order) 24 | } 25 | else{ 26 | setStopsBetween(null) 27 | setOrder(null) 28 | } 29 | },[start_station,end_station, stops, line]) 30 | 31 | return {stops:stopsBetween,order} 32 | } -------------------------------------------------------------------------------- /src/components/AboutModal/AboutModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import styled from "styled-components"; 3 | import Modal from "react-modal"; 4 | import { 5 | AboutPage, 6 | ContributeSection, 7 | DataClinicSection, 8 | ProjectInfoSection, 9 | TextColumn, 10 | } from "@dataclinic/about-page"; 11 | import { Header, Body, SubHeader } from "@dataclinic/typography"; 12 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 13 | import { faTimes } from "@fortawesome/free-solid-svg-icons"; 14 | import { AboutPageSegment, Styles } from "./AboutModalStyles"; 15 | import { useMedia } from "use-media"; 16 | import * as Fathom from "fathom-client"; 17 | 18 | const customStyles = { 19 | content: { 20 | maxWidth: "60vw", 21 | left: "20vw", 22 | maxHeight: "80vh", 23 | top: "10vh", 24 | }, 25 | overlay: { zIndex: 1000 }, 26 | }; 27 | 28 | Modal.setAppElement("#root"); 29 | 30 | type AboutModalProps = { 31 | isOpen: boolean; 32 | onClose: () => void; 33 | }; 34 | 35 | const BlackTextColumn = styled(TextColumn)` 36 | p { 37 | color: black !important; 38 | } 39 | `; 40 | const BodyBlack = styled.p` 41 | color: black !important; 42 | -webkit-font-smoothing: antialiased; 43 | font-weight: 400; 44 | font-size: 1rem; 45 | line-height: 1.875rem; 46 | font-family: "Lato", sans-serif; 47 | padding: 10px 0px; 48 | `; 49 | 50 | export function AboutModal({ isOpen, onClose }: AboutModalProps) { 51 | const smallScreen = useMedia("(max-width: 480px)"); 52 | console.log("Small screen is ", smallScreen); 53 | const contentStyle = smallScreen 54 | ? { 55 | overlay: {}, 56 | content: { 57 | width: "100%", 58 | left: "0px", 59 | top: "0px", 60 | padding: "0px", 61 | margin: "0px", 62 | }, 63 | } 64 | : { 65 | overlay: {}, 66 | content: { 67 | maxWidth: "60vw", 68 | left: "20vw", 69 | maxHeight: "80vh", 70 | top: "10vh", 71 | }, 72 | }; 73 | 74 | const customStyles = { 75 | content: contentStyle.content, 76 | overlay: { zIndex: 1000, ...contentStyle.overlay }, 77 | }; 78 | 79 | useEffect(() => { 80 | if (isOpen) { 81 | Fathom.trackPageview({ url: "/feedback" }); 82 | } 83 | }, [isOpen]); 84 | 85 | return ( 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 98 | 99 | As the city continues to adjust to the new normal and people 100 | begin heading back to work and school, a central question is how 101 | will this work given NYC commuters reliance on public 102 | transportation. Is it possible to move so many people while 103 | maintaining social distancing? 104 | 105 | 106 | To help inform this question, SubwayCrowds is designed to 107 | identify for specific trips when subway cars are likely to be 108 | most crowded so that individuals might alter their travel time 109 | or route. 110 | 111 | 112 | 113 |

114 | A multi-step heuristic approach 115 |

116 | 117 | The task of estimating the crowdedness of a train sounds 118 | straightforward yet it is anything but, especially given the 119 | limitations of publicly available data. The methodology we 120 | adopted is our best guess approximation and can be broken down 121 | into the four steps below. 122 | 123 | 131 | 132 | We've open sourced the methodology and welcome the opportunity 133 | to make improvements. To learn more, check out our{" "} 134 | 135 | repo 136 | {" "} 137 | for more details and source code. 138 | 139 |
140 | 141 | 142 |
143 |
144 |
145 |
146 | ); 147 | } 148 | -------------------------------------------------------------------------------- /src/components/AboutModal/AboutModalStyles.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const CloseButton = styled.div` 4 | cursor: pointer; 5 | position: absolute; 6 | top: 20px; 7 | right: 40px; 8 | font-size: 30px; 9 | color: rgb(255, 187, 0); 10 | `; 11 | const Header = styled.div` 12 | width: 100%; 13 | display: flex; 14 | flex-direction: row; 15 | justify-content: space-between; 16 | `; 17 | 18 | const Container = styled.div` 19 | height: 100%; 20 | overflow-y: auto; 21 | display: flex; 22 | flex-direction: column; 23 | `; 24 | 25 | const Content = styled.div` 26 | flex: 1; 27 | `; 28 | const TextColumn = styled.div` 29 | display: flex; 30 | flex-direction: column; 31 | flex: 1; 32 | justify-content: center; 33 | p { 34 | color: white !important; 35 | } 36 | h2 { 37 | color: white; 38 | padding-left: 0px; 39 | margin-bottom: 0px; 40 | padding-bottom: 0px; 41 | } 42 | `; 43 | type AboutPageSegmentProps = { 44 | color?: string; 45 | }; 46 | 47 | export const AboutPageSegment = styled.div` 48 | width:100%; 49 | box-sizing:border-box; 50 | display:flex; 51 | padding:20px; 52 | flex-direction:column!important; 53 | justify-content:center; 54 | background-color: ${({ color }) => color}; 55 | ${Header}{ 56 | color:white; 57 | } 58 | ${({ color, theme }) => 59 | !color && 60 | ` 61 | :nth-child(3n){ 62 | background-color:${theme.colors.reds.light}; 63 | } 64 | :nth-child(3n+1){ 65 | background-color:${theme.colors.oranges.normal}; 66 | } 67 | :nth-child(3n+2){ 68 | background-color:${theme.colors.greens.light}; 69 | } 70 | `}} 71 | @media ${({ theme }) => theme.devices.tablet}{ 72 | flex-direction:row; 73 | padding: 87px 60px 40px 60px 74 | } 75 | `; 76 | export const Styles = { 77 | CloseButton, 78 | Container, 79 | Content, 80 | Header, 81 | TextColumn, 82 | }; 83 | -------------------------------------------------------------------------------- /src/components/DayOfWeekSelector/DayOfWeekSelector.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Styles} from './DayOfWeekSelectorStyles' 3 | 4 | type Props={ 5 | weekday: boolean, 6 | onChange: (weekday:boolean)=>void 7 | } 8 | export function DayOfWeekSelector({weekday,onChange}: Props){ 9 | return ( 10 | 11 | onChange(true)}> 12 | onChange(true)} 16 | checked={weekday} /> 17 | weekday 18 | 19 | / 20 | onChange(false)}> 21 | onChange(false)} 25 | checked={!weekday} /> 26 | weekend 27 | 28 | 29 | ) 30 | } -------------------------------------------------------------------------------- /src/components/DayOfWeekSelector/DayOfWeekSelectorStyles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Container =styled.div` 4 | display:flex; 5 | flex-direction:row; 6 | justify-content:space-around; 7 | align-items:center; 8 | margin:5px 0px; 9 | box-sizing:border-box; 10 | padding:10px 0px; 11 | color:#27a3aa; 12 | font-weight:bold; 13 | @media only screen and (min-width:900px){ 14 | margin: 1px 0px; 15 | } 16 | ` 17 | 18 | const Option = styled.div` 19 | box-sizing:border-box; 20 | padding:10px 0px; 21 | :first-child{ 22 | margin-right:10px; 23 | } 24 | cursor:pointer; 25 | ` 26 | const Divider = styled.span` 27 | 28 | ` 29 | 30 | export const Styles={ 31 | Container, 32 | Option, 33 | Divider 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/components/FeedbackModal/FeedbackModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import Modal from "react-modal"; 3 | import { Styles } from "./FeedbackModalStyles"; 4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 5 | import { faTimes } from "@fortawesome/free-solid-svg-icons"; 6 | import { useMedia } from "use-media"; 7 | import * as Fathom from "fathom-client"; 8 | 9 | const customStyles = { 10 | overlay: { zIndex: 1000 }, 11 | content: { 12 | maxWidth: "60vw", 13 | left: "20vw", 14 | maxHeight: "80vh", 15 | top: "10vh", 16 | }, 17 | }; 18 | 19 | Modal.setAppElement("#root"); 20 | type FeedbackModalProps = { 21 | isOpen: boolean; 22 | onClose: () => void; 23 | }; 24 | export function FeedbackModal({ isOpen, onClose }: FeedbackModalProps) { 25 | useEffect(() => { 26 | if (isOpen) { 27 | Fathom.trackPageview({ url: "/feedback" }); 28 | } 29 | }, [isOpen]); 30 | 31 | const smallScreen = useMedia("(max-width: 480px)"); 32 | const contentStyle = smallScreen 33 | ? { 34 | overlay: {}, 35 | content: { 36 | width: "100%", 37 | left: "0px", 38 | top: "0px", 39 | padding: "0px", 40 | margin: "0px", 41 | }, 42 | } 43 | : { 44 | overlay: {}, 45 | content: { 46 | maxWidth: "60vw", 47 | left: "20vw", 48 | maxHeight: "80vh", 49 | top: "10vh", 50 | }, 51 | }; 52 | 53 | const customStyles = { 54 | content: contentStyle.content, 55 | overlay: { zIndex: 1000, ...contentStyle.overlay }, 56 | }; 57 | 58 | return ( 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/components/FeedbackModal/FeedbackModalStyles.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Form = styled.iframe` 4 | flex: 1; 5 | border: none; 6 | `; 7 | 8 | const CloseButton = styled.div` 9 | cursor: pointer; 10 | position: absolute; 11 | top: 20px; 12 | right: 40px; 13 | font-size: 30px; 14 | color: rgb(255, 187, 0); 15 | `; 16 | const Header = styled.div` 17 | width: 100%; 18 | display: flex; 19 | flex-direction: row; 20 | justify-content: space-between; 21 | `; 22 | 23 | const Container = styled.div` 24 | height: 100%; 25 | overflow-y: auto; 26 | display: flex; 27 | flex-direction: column; 28 | `; 29 | 30 | const Content = styled.div` 31 | flex: 1; 32 | display: flex; 33 | `; 34 | 35 | export const Styles = { 36 | Form, 37 | CloseButton, 38 | Container, 39 | Content, 40 | Header, 41 | }; 42 | -------------------------------------------------------------------------------- /src/components/HourSlider/HourSlider.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Styles } from "./HourSliderStyles"; 3 | import Slider from "@material-ui/core/Slider"; 4 | import { withStyles } from "@material-ui/core/styles"; 5 | import { am_pm_from_24 } from "../../utils"; 6 | 7 | const HSlider = withStyles({ 8 | root: { 9 | color: "#27a3aa", 10 | height: 8, 11 | }, 12 | markLabel: { 13 | fontFamily: 'lato', 14 | fontSize: '12px', 15 | color: 'grey' 16 | } 17 | })(Slider); 18 | 19 | type HourSliderProps = { 20 | hour: number; 21 | onSetHour: (hour: number) => void; 22 | }; 23 | 24 | const marks = [ 25 | { 26 | value: 2, 27 | label: '2 am', 28 | }, 29 | { 30 | value: 6, 31 | label: '6 am', 32 | }, 33 | { 34 | value: 10, 35 | label: '10 am', 36 | }, 37 | { 38 | value: 14, 39 | label: '2 pm', 40 | }, 41 | { 42 | value: 18, 43 | label: '6 pm', 44 | }, 45 | { 46 | value: 22, 47 | label: '10 pm', 48 | }, 49 | ]; 50 | 51 | export function HourSlider({ hour, onSetHour }: HourSliderProps) { 52 | const formatValue = (hour: number) => am_pm_from_24(hour); 53 | return ( 54 | 55 | , hour: number | number[]) => 64 | onSetHour(hour as number) 65 | } 66 | /> 67 | 68 | Use slider to change the start time of the trip 69 | 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/components/HourSlider/HourSliderStyles.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const HourSliderContainer = styled.div` 4 | margin-top: 5px; 5 | display: flex; 6 | flex-direction: column; 7 | width: 95%; 8 | @media only screen and (min-width: 900px) { 9 | padding-left: 70px; 10 | } 11 | height: 65px; 12 | box-sizing: border-box; 13 | `; 14 | 15 | const Caption = styled.div` 16 | color: grey; 17 | font-size: 12px; 18 | padding-bottom: 10px; 19 | @media only screen and (min-width: 900px) { 20 | margin-top: 5px; 21 | padding-left: 70px; 22 | font-size: 12px; 23 | } 24 | `; 25 | export const Styles = { 26 | HourSliderContainer, 27 | Caption, 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/HourlyChart/HourlyChart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from "react"; 2 | import { Line } from "react-chartjs-2"; 3 | import { HourlyObservation, MetricType } from "../../types"; 4 | import { am_pm_from_24, DataTypeColor } from "../../utils"; 5 | import * as ChartAnnotation from "chartjs-plugin-annotation"; 6 | 7 | type Props = { 8 | hourlyData: HourlyObservation[] | null; 9 | hour: number; 10 | }; 11 | 12 | export function HourlyChart({ hourlyData, hour }: Props) { 13 | const [width, setWidth] = useState(0); 14 | const [height, setHeight] = useState(0); 15 | const graphDiv = useRef(null); 16 | 17 | const onResize = () => { 18 | const dims = graphDiv.current?.getBoundingClientRect(); 19 | setWidth(dims ? dims.width : 0); 20 | setHeight(dims ? dims.height : 0); 21 | }; 22 | 23 | useEffect(() => { 24 | window.addEventListener("resize", onResize); 25 | return () => window.removeEventListener("resize", onResize); 26 | }, []); 27 | 28 | return ( 29 |
30 | {hourlyData ? ( 31 | <> 32 |
33 | ({ 39 | x: c.hour, 40 | y: c.numPeople, 41 | })), 42 | label: "Current", 43 | backgroundColor: DataTypeColor(MetricType.CURRENT, 0.4), 44 | borderColor: DataTypeColor(MetricType.CURRENT, 1), 45 | }, 46 | { 47 | data: hourlyData.map((c) => ({ 48 | x: c.hour, 49 | y: c.numPeopleLastMonth, 50 | })), 51 | label: "1 month ago", 52 | backgroundColor: "rgba(0,0,0,0)", 53 | borderColor: DataTypeColor(MetricType.MONTH, 1.0), 54 | }, 55 | { 56 | data: hourlyData.map((c) => ({ 57 | x: c.hour, 58 | y: c.numPeopleLastYear, 59 | })), 60 | label: "2 years ago", 61 | backgroundColor: "rgba(0,0,0,0)", 62 | borderColor: DataTypeColor(MetricType.YEAR, 1.0), 63 | }, 64 | ], 65 | 66 | labels: hourlyData.map((c) => c.hour), 67 | }} 68 | width={width} 69 | height={height} 70 | redraw={false} 71 | options={{ 72 | maintainAspectRatio: false, 73 | responsive: true, 74 | legend: { 75 | display: true, 76 | }, 77 | scales: { 78 | yAxes: [ 79 | { 80 | scaleLabel: { 81 | display: true, 82 | labelString: "Number of people", 83 | }, 84 | }, 85 | ], 86 | xAxes: [ 87 | { 88 | scaleLabel: { 89 | display: true, 90 | labelString: "Hour of day", 91 | }, 92 | ticks: { 93 | callback: (hour: number) => am_pm_from_24(hour), 94 | }, 95 | }, 96 | ], 97 | }, 98 | annotation: { 99 | annotations: [ 100 | { 101 | type: "line", 102 | mode: "vertical", 103 | scaleID: "x-axis-0", 104 | value: hour, 105 | borderColor: "#27a3aa", 106 | borderWidth: 2, 107 | }, 108 | ], 109 | }, 110 | }} 111 | plugins={[ChartAnnotation]} 112 | /> 113 |
114 | 115 | ) : ( 116 |

No data available

117 | )} 118 |
119 | ); 120 | } 121 | -------------------------------------------------------------------------------- /src/components/MetricSelector/MetricSelector.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Styles } from "./MetricSelectorStyles"; 3 | 4 | export enum MetricType { 5 | Hour = "hour", 6 | Stops = "stops", 7 | } 8 | type MetricSelectorProps = { 9 | metric: MetricType; 10 | onSetMetric: (metric: MetricType) => void; 11 | }; 12 | 13 | export function MetricSelector({ metric, onSetMetric }: MetricSelectorProps) { 14 | return ( 15 | 16 | onSetMetric(MetricType.Hour)} 19 | > 20 | By Hour 21 | 22 | onSetMetric(MetricType.Stops)} 25 | > 26 | By Stop 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/MetricSelector/MetricSelectorStyles.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const MetricSelectorContainer = styled.div` 4 | display: flex; 5 | flex-direction: row; 6 | justify-content: space-around; 7 | box-sizing: border-box; 8 | padding: 10px 20px; 9 | `; 10 | 11 | type MetricTabProps = { 12 | selected: boolean; 13 | }; 14 | 15 | const MetricTab = styled.div` 16 | font-weight: ${({ selected }: MetricTabProps) => 17 | selected ? "bold" : "normal"}; 18 | 19 | text-decoration: ${({ selected }: MetricTabProps) => 20 | selected ? "underline" : "none"}; 21 | `; 22 | 23 | export const Styles = { 24 | MetricSelectorContainer, 25 | MetricTab, 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/SentanceDropDown/SentanceDropDown.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import Styles from './SentanceDropDownStyles' 3 | import { clear } from 'console'; 4 | 5 | interface DropDownOption{ 6 | text?: string; 7 | key: string; 8 | icon?: string; 9 | 10 | } 11 | 12 | interface Props { 13 | prompt: string; 14 | options: DropDownOption[]; 15 | onSelected?: (option:string | null)=>void; 16 | selectedID?: string | null, 17 | useIcon?:boolean, 18 | active:boolean 19 | } 20 | 21 | export default function SentanceDropDown ({prompt, options,selectedID, onSelected, active, useIcon=false}: Props){ 22 | const [searchTerm, setSearchTerm] = useState('') 23 | 24 | const [showDropDown, setShowDropDown] = useState(false); 25 | const selected = options?.find(option => option.key === selectedID) 26 | 27 | const filteredOptions = searchTerm ? options.filter(option=> 28 | option.text?.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase()) 29 | || option.key?.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase()) 30 | ) : options 31 | 32 | const updateSearch = (e: React.ChangeEvent) => setSearchTerm(e.target.value) 33 | const setSelected = (option:DropDownOption)=>{ 34 | if(onSelected){ 35 | onSelected(option.key) 36 | setShowDropDown(false) 37 | } 38 | } 39 | 40 | const clear = ()=>{ 41 | if(active){ 42 | setShowDropDown(true) 43 | } 44 | if(onSelected && active){ 45 | onSelected(null) 46 | } 47 | } 48 | 49 | return( 50 | 51 | {selected ? 52 | { (selected.icon && useIcon) ? : ''}{selected.text && !useIcon ? selected.text : ''} 53 | : 54 | setShowDropDown(true)} placeholder={prompt} onChange={updateSearch} value={searchTerm}> 55 | } 56 | {showDropDown && 57 | 58 | {filteredOptions.map(option=> 59 | setSelected(option)} key={option.key}> 60 | {(option.icon && useIcon) && 61 | 62 | } 63 | {option.text && !useIcon && 64 | {option.text} 65 | } 66 | 67 | )} 68 | 69 | } 70 | 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /src/components/SentanceDropDown/SentanceDropDownStyles.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Container = styled.div` 4 | display: inline; 5 | position: relative; 6 | min-width: 50px; 7 | padding: 0px 5px; 8 | `; 9 | 10 | const Input = styled.input` 11 | border: none; 12 | box-shadow: none; 13 | text-align: center; 14 | border-bottom: 1px solid grey; 15 | `; 16 | const Icon = styled.img` 17 | width: 30px; 18 | `; 19 | const DropDownList = styled.ul` 20 | list-style: none; 21 | margin: 0px; 22 | padding: 10px 20px; 23 | box-sizing: border-box; 24 | background-color: white; 25 | box-shadow: -1px 2px 5px 0px rgba(0, 0, 0, 0.75); 26 | overflow-y: auto; 27 | max-height: 30vh; 28 | position: absolute; 29 | top: 125%; 30 | left: 0px; 31 | width: 100%; 32 | border-radius: 5px; 33 | z-index: 1000; 34 | `; 35 | 36 | const DropDownListEntry = styled.li` 37 | border-bottom: 1 px solid grey; 38 | display: flex; 39 | justify-content: center; 40 | cursor: pointer; 41 | box-sizing: border-box; 42 | padding: 10px; 43 | &:hover { 44 | background-color: #f0f0f0; 45 | } 46 | `; 47 | 48 | export default { 49 | Container, 50 | Input, 51 | DropDownList, 52 | DropDownListEntry, 53 | Icon, 54 | }; 55 | -------------------------------------------------------------------------------- /src/components/ShareButtons/ShareButtons.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { 4 | EmailShareButton, 5 | FacebookShareButton, 6 | TwitterShareButton, 7 | EmailIcon, 8 | FacebookIcon, 9 | TwitterIcon, 10 | } from 'react-share'; 11 | 12 | 13 | type Props={ 14 | startStation: string | undefined, 15 | endStation: string | undefined, 16 | line: string | undefined 17 | } 18 | export const ShareButtons = ({startStation,endStation,line}: Props)=>{ 19 | const prompt = `Checkout this graph of crowding on the ${line} line between ${startStation} and ${endStation}` 20 | const hashtag = `#nyc` // Facebook share only allows one hashtag 21 | const hashtagList = ['newyorktough', 'covid19', 'mta', 'dataforgood', 'opendata', 'nyc', 'SubwayCrowds'] 22 | 23 | return( 24 | <> 25 | 26 | 27 | {' '} 28 | 29 | 30 | {' '} 31 | 35 | 36 | 37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /src/components/SimplePassword/SimplePassword.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import {Styles} from './SimplePasswordStyles' 3 | import md5 from 'md5' 4 | 5 | type SimplePasswordProps = { 6 | onPassed: ()=>void 7 | } 8 | 9 | export const SimplePassword: React.FC = ({onPassed})=>{ 10 | const [enteredPassword, setEntredPassword] = useState('') 11 | const [attemptFailed, setAttemptFailed] = useState(false) 12 | 13 | const checkPassword= (password: string)=>{ 14 | if (md5(password)==='7959b961846e0e1355e4440d6f0b344c'){ 15 | onPassed() 16 | } 17 | else{ 18 | setAttemptFailed(true) 19 | setTimeout(()=> setAttemptFailed(false), 3000) 20 | } 21 | } 22 | 23 | return( 24 | 25 | setEntredPassword(e.target.value)} value={enteredPassword}/> 26 | checkPassword(enteredPassword)}>Submit 27 | {attemptFailed && 28 |

Sorry that password was wrong.

29 | } 30 |
31 | 32 | ) 33 | } -------------------------------------------------------------------------------- /src/components/SimplePassword/SimplePasswordStyles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const SimplePasswordContainer=styled.div` 4 | width:100vw; 5 | height:100vh; 6 | background-color:white; 7 | display:flex; 8 | flex-direction:column; 9 | justify-content:center; 10 | align-items:center; 11 | ` 12 | 13 | const PasswordInput = styled.input` 14 | width:40vw; 15 | padding: 5px 10px; 16 | box-sizing:border-box; 17 | margin-bottom: 20px; 18 | 19 | ` 20 | 21 | const PasswordSubmit = styled.button` 22 | width:40vw; 23 | ` 24 | export const Styles={ 25 | SimplePasswordContainer, 26 | PasswordInput, 27 | PasswordSubmit 28 | } -------------------------------------------------------------------------------- /src/components/StopsChart/StopsChart.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import {CrowdingObservation, Stop, MetricType} from '../../types' 3 | import {Styles} from './StopsChartStyles' 4 | import {ToggleButton} from '../ToggleButton/ToggleButton' 5 | 6 | 7 | type MaxCounts={ 8 | current:number, 9 | month: number, 10 | year: number 11 | } 12 | 13 | export enum StopChartType{ 14 | Continuous, 15 | Discrete 16 | } 17 | 18 | type Props={ 19 | stops: Stop[] | null, 20 | stopCount: CrowdingObservation[] | null, 21 | maxCounts?: MaxCounts | null 22 | variant : StopChartType 23 | } 24 | 25 | 26 | export const StopsChart = ({stops, stopCount, maxCounts, variant=StopChartType.Continuous}:Props)=>{ 27 | const [showMonth, setShowMonth] = useState(false) 28 | const [showYear, setShowYear] = useState(false) 29 | 30 | const maxStopCount = maxCounts ? Math.max(maxCounts.current, showMonth ? maxCounts.month : 0, showYear ? maxCounts.year : 0) : 0 31 | 32 | const scoreForStop = (stationID:string, metric: MetricType) => { 33 | const stop = stopCount?.find(sc=>sc.stationID === stationID) 34 | if(stop){ 35 | switch(metric){ 36 | case MetricType.CURRENT: 37 | return stop.numPeople 38 | case MetricType.MONTH: 39 | return stop.numPeopleLastMonth 40 | case MetricType.YEAR: 41 | return stop.numPeopleLastYear 42 | } 43 | } 44 | return 0 45 | } 46 | 47 | const scoreForStopMonth = (stationID:string) => { 48 | const count = stopCount?.find(sc=>sc.stationID === stationID)?.numPeopleLastMonth 49 | return count ? count : 0 50 | } 51 | 52 | const scoreForStopYear = (stationID:string) => { 53 | const count = stopCount?.find(sc=>sc.stationID === stationID)?.numPeopleLastYear 54 | return count ? count : 0 55 | } 56 | const squareBuckets = [5,10,15,20,25,30,35,40,45,50] 57 | const bucketColors = ['#fce5ab', '#fde1a0', '#fedc95', '#ffd889', '#ffd47f', '#ffd073', '#ffcc68', '#ffc85c', '#ffc44f', '#ffbf41', '#ffbb31'] 58 | 59 | const makeStopSquares = (occupancy:number)=>{ 60 | let bin : number | null = null; 61 | squareBuckets.forEach ((bucket,i)=> {if(bucket > occupancy && bin===null){ bin = i}} ) 62 | if(bin === null){ 63 | bin = squareBuckets.length 64 | } 65 | 66 | const binEdges = (bin:number)=>{ 67 | if(bin ===0 ){ 68 | return `< ${squareBuckets[bin]}` 69 | } 70 | else if (bin < squareBuckets.length ){ 71 | return `${squareBuckets[bin-1]} - ${squareBuckets[bin]}` 72 | } 73 | else{ 74 | return `> ${squareBuckets[bin-1]}` 75 | } 76 | } 77 | 78 | return <> 79 | { [...Array(bin+1)].map((_,i)=> 80 | 81 | ) 82 | } 83 | {binEdges(bin)} 84 | 85 | } 86 | 87 | return ( 88 | 89 | {variant === StopChartType.Continuous && 90 | 91 | Compare to: 92 | setShowMonth(!showMonth)}>1 month ago 93 | setShowYear(!showYear)}>1 year ago 94 | 95 | } 96 | 97 | 98 | {stops && stops.map(stop=> 99 | <> 100 | {variant === StopChartType.Continuous && 101 | <> 102 | {stop.station} 103 | 104 | 105 | {Math.floor(scoreForStop(stop?.id, MetricType.CURRENT)).toLocaleString()} 106 | 107 | {showMonth && 108 | 113 | } 114 | 115 | {showYear && 116 | 121 | } 122 | 123 | 124 | } 125 | {variant === StopChartType.Discrete && 126 | <> 127 | {stop.station} 128 | 129 | {makeStopSquares(scoreForStop(stop?.id, MetricType.CURRENT))} 130 | 131 | 132 | } 133 | 134 | )} 135 | 136 | 137 | ) 138 | } 139 | -------------------------------------------------------------------------------- /src/components/StopsChart/StopsChartStyles.tsx: -------------------------------------------------------------------------------- 1 | import styled, {css, keyframes} from 'styled-components' 2 | import {DataTypeColor} from '../../utils' 3 | import {MetricType} from '../../types' 4 | 5 | const MIN_PC_INSIDE = 10 6 | 7 | const Container = styled.div` 8 | display:flex; 9 | flex-direction:column; 10 | flex:1; 11 | @media only screen and (min-width:900px){ 12 | 13 | /* max-height: 400px; */ 14 | } 15 | ` 16 | const BarsContainer = styled.ul` 17 | display:grid; 18 | grid-template-columns: 0.6fr 1fr ; 19 | grid-template-rows: repeat(auto-fill, 30px); 20 | grid-column-gap:20px; 21 | max-height: 50vh; 22 | overflow-y:auto; 23 | list-style:none; 24 | padding:0px 0px; 25 | box-sizing:border-box; 26 | flex:1; 27 | height:100%; 28 | align-items:center; 29 | margin-top:15px; 30 | @media only screen and (min-width:900px){ 31 | margin-top:10px; 32 | } 33 | ` 34 | 35 | const StopName = styled.li` 36 | box-sizing:border-box; 37 | text-align:right; 38 | display:flex; 39 | flex-direction:column; 40 | height:30px; 41 | /* margin-bottom: 10px; */ 42 | border-right : 2px solid black; 43 | padding: 0px 10px 0px 10px; 44 | position:relative; 45 | justify-content:center; 46 | color: grey; 47 | font-weight:bold; 48 | font-size:13px; 49 | @media only screen and (min-width:900px){ 50 | font-size:16px; 51 | } 52 | span{ 53 | overflow: hidden; 54 | white-space: nowrap; 55 | text-overflow: ellipsis; 56 | } 57 | ::after{ 58 | content: ''; 59 | background-color:white; 60 | width:10px; 61 | height:10px; 62 | border-radius: 10px; 63 | border:2px solid black; 64 | display: inline-block; 65 | position:absolute; 66 | top:9px; 67 | right:-8px; 68 | z-index:100; 69 | } 70 | ` 71 | 72 | type BarProps = { 73 | percent : number, 74 | metric: MetricType, 75 | } 76 | 77 | const StopBars = styled.li` 78 | display:flex; 79 | flex-direction:column; 80 | 81 | ` 82 | const Metric = styled.div` 83 | display:flex; 84 | flex-direction:row; 85 | /* justify-content:space-between; */ 86 | align-items:center; 87 | font-size:13px; 88 | color: grey; 89 | box-sizing: border-box; 90 | padding: 8px 0px; 91 | 92 | ` 93 | 94 | 95 | const typeSizes={ 96 | [MetricType.CURRENT] : '20px', 97 | [MetricType.MONTH] : '2px', 98 | [MetricType.YEAR] : '2px' 99 | } 100 | 101 | const StopBar = styled.span` 102 | box-sizing:border-box; 103 | align-self:start; 104 | width:${({percent}:BarProps)=> `${percent}%`}; 105 | background-color:${({metric}:BarProps)=> DataTypeColor(metric,1)}; 106 | height: ${({metric}:BarProps)=> `${typeSizes[metric]}`}; 107 | box-sizing:border-box; 108 | padding:3px 3px 3px 0px; 109 | color:white; 110 | display:flex; 111 | justify-content:flex-end; 112 | font-size:0.9rem; 113 | transition: width 0.5s ease-in-out; 114 | align-items:center; 115 | span{ 116 | font-weight:bold; 117 | min-width:12px; 118 | transform: ${({percent}:BarProps)=> percent < MIN_PC_INSIDE ? 'translate(140%,0%)' : '' }}; 119 | color:${ ({percent}:BarProps)=> percent < MIN_PC_INSIDE ? 'black' : 'white' }; 120 | } 121 | ` 122 | 123 | const StopSquares = styled.div` 124 | display:flex; 125 | flex-direction:row; 126 | width:100%; 127 | align-items:center; 128 | span{ 129 | font-size: 10px; 130 | margin-left:5px; 131 | @media only screen and (min-width:900px){ 132 | margin-left: 10px; 133 | } 134 | } 135 | 136 | ` 137 | 138 | type SquareProps = { 139 | color: string 140 | order: number 141 | } 142 | 143 | const popIn = keyframes` 144 | from { 145 | transform: scale(0); 146 | } 147 | to { 148 | transform: scale(1); 149 | } 150 | ` 151 | 152 | const StopSquare = styled.div` 153 | width: 10px; 154 | height:10px; 155 | background-color:${(props:SquareProps)=> {return props.color}}; 156 | border:1px solid grey; 157 | margin-right:2px; 158 | animation : ${popIn} .2s; 159 | 160 | ` 161 | 162 | 163 | const StopCount = styled.li` 164 | align-self:end; 165 | text-align:left; 166 | ` 167 | 168 | export const Styles = { 169 | Container, 170 | BarsContainer, 171 | StopName, 172 | StopBar, 173 | StopCount, 174 | StopSquare, 175 | StopSquares, 176 | StopBars, 177 | Metric 178 | } -------------------------------------------------------------------------------- /src/components/ToggleButton/ToggleButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { MetricType } from '../../types' 3 | import {Styles} from './ToggleButtonStyles' 4 | 5 | 6 | type ToggleButtonProps = { 7 | set:boolean, 8 | metric: MetricType, 9 | onClick: (event: React.MouseEvent) => void; 10 | } 11 | export const ToggleButton: React.FC = ({set,metric,children, onClick})=>{ 12 | return( 13 | 14 | 15 | {children} 16 | 17 | ) 18 | } -------------------------------------------------------------------------------- /src/components/ToggleButton/ToggleButtonStyles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import {MetricType} from '../../types' 3 | import {DataTypeColor} from '../../utils' 4 | 5 | type ToggleButtonProps = { 6 | set:boolean 7 | } 8 | 9 | type ToggleButtonSquareProps = { 10 | metric:MetricType 11 | } 12 | 13 | const ToggleButtonSquare = styled.span` 14 | width:45px; 15 | height:15px; 16 | border: 2.5px solid; 17 | border-color: ${({metric}:ToggleButtonSquareProps)=> DataTypeColor(metric,1)}; 18 | display: inline-block; 19 | margin-right:5px; 20 | box-sizing:border-box; 21 | ` 22 | 23 | const ToggleButton = styled.button` 24 | background-color:none; 25 | text-decoration:${({set} : ToggleButtonProps) => set ? 'none' : 'line-through'}; 26 | border:none; 27 | font-size:12px; 28 | color: grey; 29 | background-color:white; 30 | display:flex; 31 | flex-direction:row; 32 | align-items:center; 33 | cursor:pointer; 34 | ` 35 | 36 | 37 | export const Styles={ 38 | ToggleButtonSquare, 39 | ToggleButton 40 | } -------------------------------------------------------------------------------- /src/components/TopBar/TopBar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Styles } from "./TopBarStyles"; 3 | import { TwoSigmaLogo } from "@dataclinic/icons"; 4 | import { Station, Line } from "../../types"; 5 | import { ShareButtons } from "../ShareButtons/ShareButtons"; 6 | 7 | type TopBarProps = { 8 | onShowFeedback: () => void; 9 | onShowAbout: () => void; 10 | startStation?: Station; 11 | endStation?: Station; 12 | line?: Line; 13 | }; 14 | 15 | export function TopBar({ 16 | onShowFeedback, 17 | onShowAbout, 18 | startStation, 19 | endStation, 20 | line, 21 | }: TopBarProps) { 22 | return ( 23 | 24 | 25 | 29 | data clinic 30 | 31 | 32 | 33 | 34 | Give us feedback 35 | 36 | 37 | 38 | 39 | About 40 | 41 | 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/components/TopBar/TopBarStyles.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const TopBar = styled.div` 4 | width: 100%; 5 | height: 20px; 6 | /* box-sizing:border-box; */ 7 | padding: 0px 0px 20px 0px; 8 | @media only screen and (min-width: 900px) { 9 | padding: 20px 0px; 10 | font-size: 20px; 11 | } 12 | display: flex; 13 | flex-direction: row; 14 | justify-content: space-between; 15 | align-items: center; 16 | font-size: 15px; 17 | button { 18 | margin-right: 5px; 19 | font-size: 15px; 20 | @media only screen and (min-width: 900px) { 21 | font-size: 20px; 22 | } 23 | } 24 | `; 25 | const ModalButton = styled.button` 26 | background: none; 27 | border: none; 28 | cursor: pointer; 29 | `; 30 | 31 | const AboutModalButton = styled.button` 32 | background: none; 33 | border: none; 34 | cursor: pointer; 35 | color: #27a3aa 36 | `; 37 | 38 | const Links = styled.div` 39 | display: flex; 40 | flex-direction: row; 41 | align-items: center; 42 | `; 43 | 44 | const DataClinicLink = styled.a` 45 | display: flex; 46 | color: black; 47 | text-decoration: none; 48 | flex-direction: row; 49 | align-items: center; 50 | color: #1e4d5e; 51 | `; 52 | 53 | export const Styles = { 54 | TopBar, 55 | ModalButton, 56 | AboutModalButton, 57 | DataClinicLink, 58 | Links, 59 | }; 60 | -------------------------------------------------------------------------------- /src/icons/giticon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsdataclinic/SubwayCrowds/759994571546349024c107c339a489e14a7439d4/src/icons/giticon.png -------------------------------------------------------------------------------- /src/icons/mediumicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsdataclinic/SubwayCrowds/759994571546349024c107c339a489e14a7439d4/src/icons/mediumicon.png -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | width:100vw; 9 | height:100vh; 10 | display:flex; 11 | flex-direction: column; 12 | } 13 | 14 | code { 15 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 16 | monospace; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import {DataProvider} from './Contexts/DataContext' 6 | 7 | import * as serviceWorker from './serviceWorker'; 8 | 9 | ReactDOM.render( 10 | 11 | 12 | 13 | 14 | , 15 | document.getElementById('root') 16 | ); 17 | 18 | // If you want your app to work offline and load faster, you can change 19 | // unregister() to register() below. Note this comes with some pitfalls. 20 | // Learn more about service workers: https://bit.ly/CRA-PWA 21 | serviceWorker.unregister(); 22 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | process.env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl, { 112 | headers: { 'Service-Worker': 'script' } 113 | }) 114 | .then(response => { 115 | // Ensure service worker exists, and that we really are getting a JS file. 116 | const contentType = response.headers.get('content-type'); 117 | if ( 118 | response.status === 404 || 119 | (contentType != null && contentType.indexOf('javascript') === -1) 120 | ) { 121 | // No service worker found. Probably a different app. Reload the page. 122 | navigator.serviceWorker.ready.then(registration => { 123 | registration.unregister().then(() => { 124 | window.location.reload(); 125 | }); 126 | }); 127 | } else { 128 | // Service worker found. Proceed as normal. 129 | registerValidSW(swUrl, config); 130 | } 131 | }) 132 | .catch(() => { 133 | console.log( 134 | 'No internet connection found. App is running in offline mode.' 135 | ); 136 | }); 137 | } 138 | 139 | export function unregister() { 140 | if ('serviceWorker' in navigator) { 141 | navigator.serviceWorker.ready 142 | .then(registration => { 143 | registration.unregister(); 144 | }) 145 | .catch(error => { 146 | console.error(error.message); 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/types.tsx: -------------------------------------------------------------------------------- 1 | export type Line = { 2 | name: string; 3 | icon: any; 4 | id: string; 5 | }; 6 | 7 | export type Station = { 8 | name: string; 9 | turnstile_name: string; 10 | lines: string[]; 11 | id: string; 12 | }; 13 | 14 | export type Stop = { 15 | line: string; 16 | station: string; 17 | id: string; 18 | order: number; 19 | }; 20 | 21 | export type HourlyObservation = { 22 | hour: number; 23 | numPeople: number; 24 | numPeopleLastYear: number; 25 | numPeopleLastMonth: number; 26 | }; 27 | 28 | export enum Direction { 29 | NORTHBOUND, 30 | SOUTHBOUND, 31 | } 32 | 33 | export type CrowdingObservation = { 34 | stationID: string; 35 | lineID: string; 36 | hour: number; 37 | numPeople: number; 38 | numPeopleLastMonth: number; 39 | numPeopleLastYear: number; 40 | direction: Direction; 41 | weekday: boolean; 42 | }; 43 | 44 | export enum MetricType { 45 | CURRENT, 46 | MONTH, 47 | YEAR, 48 | } 49 | 50 | export type RawCrowding = { 51 | STATION: string; 52 | route_id: string; 53 | hour: number; 54 | current_crowd: number; 55 | last_month_crowd: number; 56 | last_year_crowd: number; 57 | direction_id: number; 58 | weekday: number; 59 | }; 60 | 61 | export type CarsByLine = { 62 | line: string; 63 | num_cars: number; 64 | }; -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { MetricType } from "./types"; 2 | 3 | export function am_pm_from_24(hour: number) { 4 | const hour_12 = ((hour + 11) % 12) + 1; 5 | return hour > 12 - 1 ? `${hour_12} pm` : `${hour_12} am`; 6 | } 7 | 8 | export function DataTypeColor(type: MetricType, opacity: number | undefined) { 9 | const o = opacity ? opacity : 1; 10 | 11 | switch (type) { 12 | case MetricType.CURRENT: 13 | return `rgba(255,187,49,${o})`; 14 | case MetricType.MONTH: 15 | return `rgba(28,76,93,${o})`; 16 | case MetricType.YEAR: 17 | return `rgba(112,214,227,${o})`; 18 | } 19 | } 20 | 21 | export function filerTruthy(t: T | undefined): t is T { 22 | return !!t; 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react", 21 | "downlevelIteration":true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------