From 9a9f6e02b5e0ad203dfa0a90bb64c73da2b478b5 Mon Sep 17 00:00:00 2001 From: Marcin Kuthan Date: Tue, 26 Nov 2024 16:45:34 +0100 Subject: [PATCH] Add NY Taxi Trips Count page; refactor service methods and update navigation --- app.py | 10 ++- example/services/ny_tlc_trips_service.py | 33 ++++++-- example/ui/components/export_buttons.py | 4 +- .../ui/pages/ny_tlc_trips_avg_speed_page.py | 2 - example/ui/pages/ny_tlc_trips_count_page.py | 42 ++++++++++ example/ui/pages/ny_tlc_trips_totals_page.py | 30 ++----- .../services/test_ny_tlc_trips_service.py | 10 +++ .../ui/pages/test_ny_tlc_trips_count_page.py | 84 +++++++++++++++++++ .../ui/pages/test_ny_tlc_trips_totals_page.py | 7 -- 9 files changed, 180 insertions(+), 42 deletions(-) create mode 100644 example/ui/pages/ny_tlc_trips_count_page.py create mode 100644 tests/example/ui/pages/test_ny_tlc_trips_count_page.py diff --git a/app.py b/app.py index ea122c4..e9d6a26 100644 --- a/app.py +++ b/app.py @@ -66,15 +66,19 @@ def share(path: str) -> None: else: home = st.Page("example/ui/pages/home_page.py", title="Home", icon=":material/home:", default=True) - nyt_tlc_trips_totals = st.Page( + ny_tlc_trips_totals = st.Page( "example/ui/pages/ny_tlc_trips_totals_page.py", title="NY Taxi Trips Totals", icon=":material/local_taxi:" ) - nyt_tlc_trips_avg_speed = st.Page( + ny_tlc_trips_count = st.Page( + "example/ui/pages/ny_tlc_trips_count_page.py", title="NY Taxi Trips Count", icon=":material/analytics:" + ) + + ny_tlc_trips_avg_speed = st.Page( "example/ui/pages/ny_tlc_trips_avg_speed_page.py", title="NY Taxi Trips Avg Speed", icon=":material/speed:" ) - pg = st.navigation(pages=[home, nyt_tlc_trips_totals, nyt_tlc_trips_avg_speed]) + pg = st.navigation(pages=[home, ny_tlc_trips_totals, ny_tlc_trips_count, ny_tlc_trips_avg_speed]) if st.sidebar.button("Share", icon=":material/link:"): share(pg.url_path) diff --git a/example/services/ny_tlc_trips_service.py b/example/services/ny_tlc_trips_service.py index 2e28d69..aab2b36 100644 --- a/example/services/ny_tlc_trips_service.py +++ b/example/services/ny_tlc_trips_service.py @@ -27,13 +27,25 @@ def trips_totals(date_range: tuple[date, date], payment_type: str) -> pd.DataFrame: - df = ny_tlc_trips_repository.trips_totals(date_range) - - df["payment_type"] = df["payment_type"].fillna(0).astype(int).map(_PAYMENT_TYPES) - df["rate_code"] = df["rate_code"].fillna(0).astype(int).map(_RATE_CODES) + df = _trips_totals(date_range) df = df[df["payment_type"] == payment_type] - df = df.sort_values("day") + + return df + + +def trips_count_by_payment_type(date_range: tuple[date, date]) -> pd.DataFrame: + df = _trips_totals(date_range) + + df = df.groupby(["day", "payment_type"]).agg(sum).reset_index() + + return df + + +def trips_count_by_rate_code(date_range: tuple[date, date]) -> pd.DataFrame: + df = _trips_totals(date_range) + + df = df.groupby(["day", "rate_code"]).agg(sum).reset_index() return df @@ -46,3 +58,14 @@ def trips_avg_speed(date_range: tuple[date, date]) -> pd.DataFrame: df = df.sort_values("day") return df + + +def _trips_totals(date_range: tuple[date, date]) -> pd.DataFrame: + df = ny_tlc_trips_repository.trips_totals(date_range) + + df["payment_type"] = df["payment_type"].fillna(0).astype(int).map(_PAYMENT_TYPES) + df["rate_code"] = df["rate_code"].fillna(0).astype(int).map(_RATE_CODES) + + df = df.sort_values("day") + + return df diff --git a/example/ui/components/export_buttons.py b/example/ui/components/export_buttons.py index 7061c11..fee551a 100644 --- a/example/ui/components/export_buttons.py +++ b/example/ui/components/export_buttons.py @@ -9,9 +9,9 @@ def show_export_csv(df: pd.DataFrame, filename: str) -> bool: data = dataframe_exporter.export_to_csv(df) - return st.download_button("Export CSV", data, filename, _CSV_MIME_TYPE) + return st.download_button("Export CSV", data, filename, _CSV_MIME_TYPE, icon=":material/file_download:") def show_export_excel(df: pd.DataFrame, filename: str) -> bool: data = dataframe_exporter.export_to_xls(df) - return st.download_button("Export XLS", data, filename, _XLSX_MIME_TYPE) + return st.download_button("Export XLS", data, filename, _XLSX_MIME_TYPE, icon=":material/file_download:") diff --git a/example/ui/pages/ny_tlc_trips_avg_speed_page.py b/example/ui/pages/ny_tlc_trips_avg_speed_page.py index 0d4ac59..3f42df4 100644 --- a/example/ui/pages/ny_tlc_trips_avg_speed_page.py +++ b/example/ui/pages/ny_tlc_trips_avg_speed_page.py @@ -6,8 +6,6 @@ st.title("NY Taxi Trips Average Speed") -st.write("New York City Taxi and Limousine Commission (TLC) trips") - st.caption("Filters") date_range = date_range_picker.show() diff --git a/example/ui/pages/ny_tlc_trips_count_page.py b/example/ui/pages/ny_tlc_trips_count_page.py new file mode 100644 index 0000000..a6f5c45 --- /dev/null +++ b/example/ui/pages/ny_tlc_trips_count_page.py @@ -0,0 +1,42 @@ +import altair as alt +import streamlit as st + +from example.services import ny_tlc_trips_service +from example.ui.components import date_range_picker + +st.title("NY Taxi Trips Count") + +st.caption("Filters") + +date_range = date_range_picker.show() + +st.caption("Search results") + +x_axis = alt.X("day:T", title="Day") +y_axis = alt.Y("trip_count:Q", title="Trip Count") + +trips_count_by_payment_type = ny_tlc_trips_service.trips_count_by_payment_type(date_range) + +if trips_count_by_payment_type.empty: + st.write("No trips found") +else: + trip_count_by_payment_type_chart = ( + alt.Chart(trips_count_by_payment_type) + .mark_line() + .encode(x=x_axis, y=y_axis, color=alt.Color("payment_type:N", title="Payment Type")) + .properties(title="Trip Count Over Time by Payment Type") + ) + st.altair_chart(trip_count_by_payment_type_chart, use_container_width=True) + +trips_count_by_rate_code = ny_tlc_trips_service.trips_count_by_rate_code(date_range) + +if trips_count_by_rate_code.empty: + st.write("No trips found") +else: + trip_count_by_rate_code_chart = ( + alt.Chart(trips_count_by_rate_code) + .mark_line() + .encode(x=x_axis, y=y_axis, color=alt.Color("rate_code:N", title="Rate Code")) + .properties(title="Trip Count Over Time by Rate Code") + ) + st.altair_chart(trip_count_by_rate_code_chart, use_container_width=True) diff --git a/example/ui/pages/ny_tlc_trips_totals_page.py b/example/ui/pages/ny_tlc_trips_totals_page.py index e13c90c..6c90e88 100644 --- a/example/ui/pages/ny_tlc_trips_totals_page.py +++ b/example/ui/pages/ny_tlc_trips_totals_page.py @@ -1,4 +1,3 @@ -import altair as alt import streamlit as st from example.services import ny_tlc_trips_service @@ -6,34 +5,19 @@ st.title("NY Taxi Trips Totals") -st.write("New York City Taxi and Limousine Commission (TLC) trips") - st.caption("Filters") date_range = date_range_picker.show() payment_type = payment_type_selector.show() -trips = ny_tlc_trips_service.trips_totals(date_range, payment_type) - st.caption("Search results") -if trips.empty: +trips_totals = ny_tlc_trips_service.trips_totals(date_range, payment_type) + +if trips_totals.empty: st.write("No trips found") else: - st.dataframe(trips, use_container_width=True) - - # TODO: make component for buttons in a single row - csv, xls = st.columns(2) - with csv: - export_buttons.show_export_csv(trips, "ny_tlc_trips.csv") - with xls: - export_buttons.show_export_excel(trips, "ny_tlc_trips.xlsx") - - trip_count_chart = ( - alt.Chart(trips) - .mark_line() - .encode(x="day:T", y="trip_count:Q", color="rate_code:N") - .properties(title="Trip Count Over Time by Rate Code") - ) - - st.altair_chart(trip_count_chart, use_container_width=True) + st.dataframe(trips_totals, height=800, use_container_width=True) + + export_buttons.show_export_csv(trips_totals, "ny_tlc_trips.csv") + export_buttons.show_export_excel(trips_totals, "ny_tlc_trips.xlsx") diff --git a/tests/example/services/test_ny_tlc_trips_service.py b/tests/example/services/test_ny_tlc_trips_service.py index 9bf091a..61669df 100644 --- a/tests/example/services/test_ny_tlc_trips_service.py +++ b/tests/example/services/test_ny_tlc_trips_service.py @@ -50,6 +50,16 @@ def test_trips_totals(mock_trips_totals, any_date_range): # noqa: ARG001 pd.testing.assert_frame_equal(results, expected) +def test_trips_count_by_payment_type(): + # TODO: Implement this test + pass + + +def test_trips_count_by_rate_code(): + # TODO: Implement this test + pass + + @pytest.fixture def mock_trips_avg_speed(): with patch("example.repositories.ny_tlc_trips_repository.trips_avg_speed") as mock: diff --git a/tests/example/ui/pages/test_ny_tlc_trips_count_page.py b/tests/example/ui/pages/test_ny_tlc_trips_count_page.py new file mode 100644 index 0000000..f7ee240 --- /dev/null +++ b/tests/example/ui/pages/test_ny_tlc_trips_count_page.py @@ -0,0 +1,84 @@ +from unittest.mock import patch + +import pandas as pd +import pytest +from streamlit.testing.v1 import AppTest + + +@pytest.fixture +def page_under_test(): + return "example/ui/pages/ny_tlc_trips_count_page.py" + + +@pytest.fixture +def mock_trips_count_by_payment_type(): + with patch("example.services.ny_tlc_trips_service.trips_count_by_payment_type") as mock: + yield mock + + +@pytest.fixture +def mock_trips_count_by_rate_code(): + with patch("example.services.ny_tlc_trips_service.trips_count_by_rate_code") as mock: + yield mock + + +@pytest.fixture +def mock_trips_count_by_payment_type_empty(mock_trips_count_by_payment_type): + mock_trips_count_by_payment_type.return_value = pd.DataFrame() + yield mock_trips_count_by_payment_type + + +@pytest.fixture +def mock_trips_count_by_rate_code_empty(mock_trips_count_by_rate_code): + mock_trips_count_by_rate_code.return_value = pd.DataFrame() + yield mock_trips_count_by_rate_code + + +def test_show_title_and_no_results( + page_under_test, + mock_trips_count_by_payment_type_empty, # noqa: ARG001 + mock_trips_count_by_rate_code_empty, # noqa: ARG001 +): + at = AppTest.from_file(page_under_test).run() + + assert at.title[0].value == "NY Taxi Trips Count" + + assert "No trips found" in [el.value for el in at.markdown] + + assert len(at.get("arrow_vega_lite_chart")) == 0 + + +@pytest.fixture +def mock_trips_count_by_payment_type_single(mock_trips_count_by_payment_type): + mock_trips_count_by_payment_type.return_value = pd.DataFrame( + { + "day": ["2023-01-01"], + "payment_type": "Credit card", + "total_fare": [100.0], + "total_tips": [10.0], + "total_amount": [110.0], + "trip_count": [1], + } + ) + yield mock_trips_count_by_payment_type + + +@pytest.fixture +def mock_trip_count_by_rate_code_single(mock_trips_count_by_rate_code): + mock_trips_count_by_rate_code.return_value = pd.DataFrame( + { + "day": ["2023-01-01"], + "rate_code": "Standard rate", + "total_fare": [100.0], + "total_tips": [10.0], + "total_amount": [110.0], + "trip_count": [1], + } + ) + yield mock_trips_count_by_rate_code + + +def test_show_line_chart(page_under_test, mock_trips_count_by_payment_type_single, mock_trip_count_by_rate_code_single): # noqa: ARG001 + at = AppTest.from_file(page_under_test).run() + + assert len(at.get("arrow_vega_lite_chart")) == 2 diff --git a/tests/example/ui/pages/test_ny_tlc_trips_totals_page.py b/tests/example/ui/pages/test_ny_tlc_trips_totals_page.py index 33b9e7f..4622475 100644 --- a/tests/example/ui/pages/test_ny_tlc_trips_totals_page.py +++ b/tests/example/ui/pages/test_ny_tlc_trips_totals_page.py @@ -32,7 +32,6 @@ def test_show_title_and_no_results(page_under_test, mock_trips_totals_empty): # assert len(at.dataframe) == 0 assert len(at.get("download_button")) == 0 - assert len(at.get("arrow_vega_lite_chart")) == 0 @pytest.fixture @@ -60,9 +59,3 @@ def test_show_export_buttons(page_under_test, mock_trips_totals_single): # noqa at = AppTest.from_file(page_under_test).run() assert len(at.get("download_button")) == 2 - - -def test_show_line_chart(page_under_test, mock_trips_totals_single): # noqa: ARG001 - at = AppTest.from_file(page_under_test).run() - - assert len(at.get("arrow_vega_lite_chart")) == 1