├── .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 | [](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 | You need to enable JavaScript to run this app.
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 |
--------------------------------------------------------------------------------