Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove stations submodule #191

Merged
merged 5 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 18 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,31 @@ This package is part of [Pledge4Future](https://pledge4future.org/), a project t

## :computer: Installation

`co2calculator` is currently only available on GitHub. To use the code, clone the repository as usual, for example with:
### With pip (recommended)

```
git clone https://github.com/pledge4future/co2calculator.git
```
PyPi release coming soon!

The repository has a submodule (https://github.com/trainline-eu/stations). This has to be pulled with the following command:
<!--The recommended way to install this package is with pip from the Python Package Index ([PyPi](https://pypi.org/)):

```
git submodule update --init --recursive
```
pip install -U co2calculator
```-->

### From source

This package requires Python 3.9 and the packages listed in `requirements.txt`
To install this package from source, clone the repository as usual, for example with:

```
$ pip install -r requirements.txt
```
git clone https://github.com/pledge4future/co2calculator.git
cd co2calculator
```

This package requires Python 3.10, 3.11 or 3.12 and can be installed using [poetry](https://python-poetry.org/). You can install it in your (virtual) environment with:

```
$ pip install -U poetry
$ poetry install --no-root
```


## ⌨ How to Use
Expand All @@ -48,11 +54,6 @@ The CO<sub>2</sub> Calculator uses the [OpenRouteService (ORS) API](https://open

If you want to contribute to this project, please fork this repository and create a pull request with your suggested changes.

Running the unit tests and applying the pre-commit hooks requires installing the packages listed in `requirements-dev.txt`.

```
$ pip install -r requirements-dev.txt
```

### Install pre-commit hooks

Expand Down Expand Up @@ -94,9 +95,10 @@ $ pytest
- Detour coefficients for train trips (1.2) and bus trips (1.5):
- Adapted from [GES 1point5](https://labos1point5.org/ges-1point5), who were advised by Frédéric Héran (economist and urban planner).

### Airports
### Airports and Train Stations

- [OurAirports](https://ourairports.com/data/)
- [Stations - A Database of European Train Stations (Trainline EU)](https://github.com/trainline-eu/stations)

## 🤝 Project partners

Expand Down
9 changes: 2 additions & 7 deletions co2calculator/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@
import enum

import iso3166
import pandas as pd


DF_AIRPORTS = pd.read_csv(
"https://davidmegginson.github.io/ourairports-data/airports.csv"
)
from co2calculator.data_handlers import Airports


class HeatingFuel(enum.Enum):
Expand Down Expand Up @@ -224,7 +219,7 @@ def __get_validators__(cls):

@classmethod
def validate_iata_code(cls, iata_code: str) -> str:
if iata_code in DF_AIRPORTS["iata_code"].values:
if iata_code in Airports().airports["iata_code"].values:
return iata_code
else:
raise ValueError(f"{iata_code} was not found in airport database")
13 changes: 13 additions & 0 deletions co2calculator/data_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,19 @@ def __init__(self):
)


class EUTrainStations:
def __init__(self):
"""Init"""
stations = pd.read_csv(
"https://raw.githubusercontent.com/trainline-eu/stations/master/stations.csv",
sep=";",
low_memory=False,
usecols=[0, 1, 2, 5, 6, 8],
)
# remove stations with no coordinates
self.stations = stations.dropna(subset=["latitude", "longitude"])


class DetourFactors:
def __init__(self, data_dir=script_path):
"""Init"""
Expand Down
27 changes: 11 additions & 16 deletions co2calculator/distances.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@
CountryCode3,
CountryName,
IataAirportCode,
DF_AIRPORTS,
DetourCoefficient,
DetourConstant,
RangeCategory,
RoutingProfile,
)
from .data_handlers import Airports, EUTrainStations

load_dotenv() # take environment variables from .env.

Expand Down Expand Up @@ -161,10 +161,11 @@ def geocoding_airport(iata: IataAirportCode) -> Tuple[str, Tuple[float, float],
:return: name, coordinates and country of the found airport
:rtype: Tuple[str, Tuple[float, float], str]
"""
df_airports = Airports().airports

airport = Airport(iata_code=iata)
name, lat, lon, country = (
DF_AIRPORTS[DF_AIRPORTS.iata_code == airport.iata_code][
df_airports[df_airports.iata_code == airport.iata_code][
["name", "latitude_deg", "longitude_deg", "iso_country"]
]
.values.flatten()
Expand Down Expand Up @@ -297,35 +298,29 @@ def geocoding_train_stations(loc_dict):

:return: Name, country and coordinates of the found location
"""
eu_train_stations = EUTrainStations().stations
station = TrainStation(**loc_dict)

stations_df = pd.read_csv(
f"{script_path}/../data/stations/stations.csv",
sep=";",
low_memory=False,
usecols=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
)
# remove stations with no coordinates
stations_df.dropna(subset=["latitude", "longitude"], inplace=True)
countries_eu = stations_df["country"].unique()
country_code = station.country
countries_eu = eu_train_stations["country"].unique()

if country_code not in countries_eu:
warnings.warn(
"The provided country is not within Europe. "
"Please provide the address of the station instead of the station name for accurate results."
)

# filter stations by country
stations_in_country_df = stations_df[stations_df["country"] == country_code]
stations_in_country = eu_train_stations[
eu_train_stations["country"] == country_code
]

# use thefuzz to find best match
choices = stations_in_country_df["slug"].values
choices = stations_in_country["slug"].values
res_station_slug, score = process.extractOne(
station.station_name, choices, scorer=fuzz.partial_ratio
)
res_station = stations_in_country_df[
stations_in_country_df["slug"] == res_station_slug
]
res_station = stations_in_country[stations_in_country["slug"] == res_station_slug]
res_country, res_station_name = res_station[["country", "name"]].values[0]

coords = (res_station.iloc[0]["latitude"], res_station.iloc[0]["longitude"])
Expand Down
1 change: 0 additions & 1 deletion data/stations
Submodule stations deleted from 7dd9b5
3 changes: 1 addition & 2 deletions tests/test_distances.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def test_valid_geocoding_dict():


@pytest.mark.skip(reason="API Key missing for test setup. TODO: Mock Response")
def test_invalid_geocoding_dict():
def test_invalid_structured_geocoding_dict():
"""Test if a providing an invalid geocoding raises an error"""
# Given parameters
loc_dict = {
Expand Down Expand Up @@ -141,7 +141,6 @@ def test_geocoding_train_stations_invalid_country():
assert e.type is ValidationError


@pytest.mark.skip(reason="API Key missing for test setup. TODO: Mock Response")
def test_geocoding_train_stations():
"""Test geocoding of European train station"""
# Given parameters
Expand Down
22 changes: 21 additions & 1 deletion tests/unit/test_data_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from pathlib import Path
import pytest
from co2calculator.data_handlers import EmissionFactors
from co2calculator.data_handlers import EmissionFactors, Airports, EUTrainStations
from co2calculator.parameters import HeatingEmissionParameters
import pandas as pd

Expand All @@ -17,8 +17,28 @@ def emission_factors_test():
return EmissionFactors(data_dir=test_data_dir)


@pytest.fixture
def airports_test():
return Airports()


@pytest.fixture
def eu_train_stations_test():
return EUTrainStations()


def test_load_emission_factors(emission_factors_test):
"""Test if the emission factors are loaded correctly"""
assert isinstance(emission_factors_test.heating, pd.DataFrame)
assert isinstance(emission_factors_test.electricity, pd.DataFrame)
assert isinstance(emission_factors_test.transport, pd.DataFrame)


def test_load_airports(airports_test):
"""Test if the airports are loaded correctly"""
assert isinstance(airports_test.airports, pd.DataFrame)


def test_load_train_stations(eu_train_stations_test):
"""Test if the train stations are loaded correctly"""
assert isinstance(eu_train_stations_test.stations, pd.DataFrame)
Loading