From e491f0ccb764f10379fdc5e5f679b0f3173ce346 Mon Sep 17 00:00:00 2001 From: simei94 Date: Wed, 27 Mar 2024 14:54:54 -0600 Subject: [PATCH 01/12] bike link analysis --- .../matsim/analysis/BikeLinksAnalysis.java | 245 ++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 src/main/java/org/matsim/analysis/BikeLinksAnalysis.java diff --git a/src/main/java/org/matsim/analysis/BikeLinksAnalysis.java b/src/main/java/org/matsim/analysis/BikeLinksAnalysis.java new file mode 100644 index 0000000..a9bec74 --- /dev/null +++ b/src/main/java/org/matsim/analysis/BikeLinksAnalysis.java @@ -0,0 +1,245 @@ +package org.matsim.analysis; + +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.TransportMode; +import org.matsim.api.core.v01.events.LinkEnterEvent; +import org.matsim.api.core.v01.events.LinkLeaveEvent; +import org.matsim.api.core.v01.events.VehicleEntersTrafficEvent; +import org.matsim.api.core.v01.events.VehicleLeavesTrafficEvent; +import org.matsim.api.core.v01.events.handler.LinkEnterEventHandler; +import org.matsim.api.core.v01.events.handler.LinkLeaveEventHandler; +import org.matsim.api.core.v01.events.handler.VehicleEntersTrafficEventHandler; +import org.matsim.api.core.v01.events.handler.VehicleLeavesTrafficEventHandler; +import org.matsim.api.core.v01.network.Link; +import org.matsim.api.core.v01.network.Network; +import org.matsim.application.MATSimAppCommand; +import org.matsim.core.api.experimental.events.EventsManager; +import org.matsim.core.events.EventsUtils; +import org.matsim.core.network.NetworkUtils; +import org.matsim.vehicles.Vehicle; +import picocli.CommandLine; + +import java.io.File; +import java.io.FileWriter; +import java.nio.file.Path; +import java.util.*; + +import static org.matsim.application.ApplicationUtils.globFile; + +@CommandLine.Command(name = "bike-links", description = "Analyze and check vehicles, which travel on bike links created for the lane repurposing scenario.") +public class BikeLinksAnalysis implements MATSimAppCommand, LinkEnterEventHandler, LinkLeaveEventHandler, VehicleEntersTrafficEventHandler, VehicleLeavesTrafficEventHandler { + + Logger log = LogManager.getLogger(BikeLinksAnalysis.class); + + @CommandLine.Option(names = "--dir", description = "Path to run directory.") + private Path runDir; + @CommandLine.Option(names = "--output", description = "Path to output directory.", defaultValue = "/analysis/repurposeLanes") + private String output; + + private Map, List>> carsOnBikeLinks = new HashMap<>(); +// this needs to be a list instead of a map, because we need it ordered + private Map, List, Double>>> bikeTravelTimes = new HashMap<>(); + private Map, Map, Double>> linkLeaveTimes = new HashMap<>(); + List bikeData = new ArrayList<>(); + private Network network; + + public static void main(String[] args) { + new BikeLinksAnalysis().execute(args); + } + + @Override + public Integer call() throws Exception { + + EventsManager manager = EventsUtils.createEventsManager(); + manager.addHandler(this); + + manager.initProcessing(); + + String eventsPath = globFile(runDir, "*output_events.*").toString(); + String networkPath = globFile(runDir, "*output_network.*").toString(); + + this.network = NetworkUtils.readNetwork(networkPath); + + EventsUtils.readEvents(manager, eventsPath); + + manager.finishProcessing(); + +// write csv files + File analysisDir = new File(runDir.toString() + output); + + if (!analysisDir.exists()) { + analysisDir.mkdirs(); + } + +// write cars on bike links csv + try (CSVPrinter printer = new CSVPrinter(new FileWriter(analysisDir.getPath() + "/cars_on_bike_only_links.csv"), CSVFormat.DEFAULT)) { + printer.printRecord("vehicleId", "linkId"); + + for (Map.Entry, List>> e : this.carsOnBikeLinks.entrySet()) { + String vehId = e.getKey().toString(); + + for (Id l : e.getValue()) { + printer.printRecord(vehId, l); + } + } + } + +// write bike stats on bike links + try (CSVPrinter printer = new CSVPrinter(new FileWriter(analysisDir.getPath() + "/bikes_on_bike_links_stats.csv"), CSVFormat.DEFAULT)) { + printer.printRecord("vehicleId", "travelTime [s]", "travelDistance [m]", "avgSpeed [m/s]", "totalTravelTime [s]", + "totalTravelDistance [m]", "totalAvgSpeed [m/s]", "shareTravelTimeOnBikeOnlyLinks", "shareTravelDistOnBikeOnlyLinks"); + + for (BikeData data : this.bikeData) { + printer.printRecord(data.bikeId, data.travelTime, data.travelDist, data.avgSpeed, data.totalTravelTime, data.totalTravelDist, data.totalAvgSpeed, + data.shareTravelTimeBikeLink, data.shareTravelDistBikeLink); + } + } + + return 0; + } + + @Override + public void handleEvent(LinkLeaveEvent event) { +// this event is only needed for getting travel times of agents who travel only one link + Id id = event.getVehicleId(); + + if (id.toString().contains(TransportMode.bike)) { + linkLeaveTimes.putIfAbsent(id, new HashMap<>()); + linkLeaveTimes.get(id).put(event.getLinkId(), event.getTime()); + } + + } + + @Override + public void handleEvent(LinkEnterEvent event) { + registerVehicle(event.getVehicleId(), event.getLinkId(), event.getTime()); + } + + @Override + public void handleEvent(VehicleEntersTrafficEvent event) { + registerVehicle(event.getVehicleId(), event.getLinkId(), event.getTime()); + } + + @Override + public void handleEvent(VehicleLeavesTrafficEvent event) { + if (event.getVehicleId().toString().contains(TransportMode.bike) && bikeTravelTimes.containsKey(event.getVehicleId())) { +// calc stats and put into data list + Id vehId = event.getVehicleId(); + + List, Double>> bikeTravels = bikeTravelTimes.get(vehId); + + + double travelTime = 0; + double travelDist = 0; + + List, Double>> filtered = bikeTravels.stream().filter(en -> en.getKey().toString().contains("bike_")).toList(); + + if (filtered.isEmpty()) { +// do nothing + } + + for (Map.Entry, Double> entry : filtered) { + if (bikeTravels.indexOf(entry) != 0) { + travelTime += entry.getValue() - bikeTravels.get(bikeTravels.indexOf(entry) - 1).getValue(); + travelDist += network.getLinks().get(entry.getKey()).getLength(); + } else { + travelTime += linkLeaveTimes.get(vehId).get(filtered.get(0).getKey()) - filtered.get(0).getValue(); + travelDist += network.getLinks().get(filtered.get(0).getKey()).getLength(); + } + } + + double avgSpeed; + + if (travelTime > 0) { + avgSpeed = travelDist /travelTime; + } else { + avgSpeed = 0; + } + + if (travelTime < 0) { + log.error("Travel time {} for vehicle {} is <= 0, this should not happen.", travelTime, vehId); + throw new IllegalArgumentException(); + } else if (travelTime > 0) { + bikeData.add(getAllStats(vehId, travelTime, travelDist, avgSpeed, bikeTravels)); + } + + bikeTravelTimes.remove(event.getVehicleId()); + linkLeaveTimes.remove(event.getVehicleId()); + } + } + + private BikeData getAllStats(Id vehId, double travelTime, double travelDist, double avgSpeed, List, Double>> bikeTravels) { + double totalTravelTime = 0; + double totalTravelDist = 0; + + for (Map.Entry, Double> entry : bikeTravels) { + if (bikeTravels.indexOf(entry) != 0) { + totalTravelTime += entry.getValue() - bikeTravels.get(bikeTravels.indexOf(entry) - 1).getValue(); + totalTravelDist += network.getLinks().get(entry.getKey()).getLength(); + } + } + + double totalAvgSpeed = 0; + double shareTravelTime = 0; + double shareTravelDist = 0; + + if (totalTravelTime > 0) { + totalAvgSpeed = totalTravelDist /totalTravelTime; + shareTravelTime = travelTime / totalTravelTime; + } + + if (totalTravelDist > 0) { + shareTravelDist = travelDist / totalTravelDist; + } + + return new BikeData(vehId, travelTime, travelDist, avgSpeed, totalTravelTime, totalTravelDist, totalAvgSpeed, + shareTravelTime, shareTravelDist); + } + + private void registerVehicle(Id vehId, Id linkId, double time) { + + if (vehId.toString().contains(TransportMode.bike)) { + bikeTravelTimes.putIfAbsent(vehId, new ArrayList<>()); + bikeTravelTimes.get(vehId).add(new AbstractMap.SimpleEntry<>(linkId, time)); + } + + if (linkId.toString().contains(TransportMode.bike)) { + Link link = this.network.getLinks().get(linkId); + + if (link.getAllowedModes().contains(TransportMode.bike) && !link.getAllowedModes().contains(TransportMode.car) + && vehId.toString().contains(TransportMode.car)) { + carsOnBikeLinks.putIfAbsent(vehId, new ArrayList<>()); + carsOnBikeLinks.get(vehId).add(linkId); + } + } + } + + private static final class BikeData { + Id bikeId; + double travelTime; + double travelDist; + double avgSpeed; + double totalTravelTime; + double totalTravelDist; + double totalAvgSpeed; + double shareTravelTimeBikeLink; + double shareTravelDistBikeLink; + + private BikeData(Id bikeId, double travelTime, double travelDist, double avgSpeed, double totalTravelTime, + double totalTravelDist, double totalAvgSpeed, double shareTravelTimeBikeLink, double shareTravelDistBikeLink) { + this.bikeId = bikeId; + this.travelTime = travelTime; + this.travelDist = travelDist; + this.avgSpeed = avgSpeed; + this.totalTravelTime = totalTravelTime; + this.totalTravelDist = totalTravelDist; + this.totalAvgSpeed = totalAvgSpeed; + this.shareTravelTimeBikeLink = shareTravelTimeBikeLink; + this.shareTravelDistBikeLink = shareTravelDistBikeLink; + } + } +} From 768e35c13d26af5f1f7b49ae9ccd76530dcd24dd Mon Sep 17 00:00:00 2001 From: simei94 Date: Wed, 27 Mar 2024 18:31:22 -0600 Subject: [PATCH 02/12] r analysis on modal shift base -> repurpose lanes --- .../R/analysis/modal_shift_repurpose_lanes.R | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/main/R/analysis/modal_shift_repurpose_lanes.R diff --git a/src/main/R/analysis/modal_shift_repurpose_lanes.R b/src/main/R/analysis/modal_shift_repurpose_lanes.R new file mode 100644 index 0000000..9153d8d --- /dev/null +++ b/src/main/R/analysis/modal_shift_repurpose_lanes.R @@ -0,0 +1,68 @@ +library(tidyverse) +library(matsim) +library(ggalluvial) +library(ggplot2) + +# TODO change path to cluster output folder of repurpose lanes scenario. + make base trips path relative to wd +setwd("Y:/net/ils/matsim-mexico-city/case-studies/lane-repurposing/output/output-mexico-city-v1.0-1pct-lane-repurposing") + +baseTripsFile <- list.files(path = "../../../baseCase/output/output-mexico-city-v1.0-1pct", pattern = "output_trips.csv.gz", full.names = TRUE) +repurposeLanesTripsFile <- list.files(pattern = "output_trips.csv.gz") + + + +# not using matsim read trips table function because right now it cannot handle delimiters different from ";" +trips_base <- read_delim(baseTripsFile, + locale = locale(decimal_mark = "."), + n_max = Inf, + col_types = cols( + start_x = col_character(), + start_y = col_character(), + end_x = col_character(), + end_y = col_character(), + end_link = col_character(), + start_link = col_character() + )) + +trips_base <- trips_base %>% + mutate( + start_x = as.double(start_x), + start_y = as.double(start_y), + end_x = as.double(end_x), + end_y = as.double(end_y) + ) +attr(trips_base,"table_name") <- baseTripsFile + +trips_repurpose_lanes <- read_delim(repurposeLanesTripsFile, + locale = locale(decimal_mark = "."), + n_max = Inf, + col_types = cols( + start_x = col_character(), + start_y = col_character(), + end_x = col_character(), + end_y = col_character(), + end_link = col_character(), + start_link = col_character() + )) + +trips_repurpose_lanes <- trips_repurpose_lanes %>% + mutate( + start_x = as.double(start_x), + start_y = as.double(start_y), + end_x = as.double(end_x), + end_y = as.double(end_y) + ) +attr(trips_repurpose_lanes,"table_name") <- repurposeLanesTripsFile + +analysisDir <- "analysis/repurposeLanes" + +full_path <- file.path(getwd(), analysisDir) +if (!dir.exists(full_path)) { + dir.create(full_path, recursive = TRUE) + cat("Created directory:", full_path, "\n") +} else { + cat("Directory already exists:", full_path, "\n") +} + +plotModalShiftSankey(trips_base, trips_repurpose_lanes, dump.output.to = analysisDir) + From a95f2cffabd59ef88b5be112511e3511572ac74f Mon Sep 17 00:00:00 2001 From: simei94 Date: Wed, 27 Mar 2024 18:31:22 -0600 Subject: [PATCH 03/12] r analysis on modal shift base -> repurpose lanes --- src/main/R/analysis/modal_shift_repurpose_lanes.R | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/R/analysis/modal_shift_repurpose_lanes.R b/src/main/R/analysis/modal_shift_repurpose_lanes.R index 9153d8d..7093175 100644 --- a/src/main/R/analysis/modal_shift_repurpose_lanes.R +++ b/src/main/R/analysis/modal_shift_repurpose_lanes.R @@ -3,7 +3,6 @@ library(matsim) library(ggalluvial) library(ggplot2) -# TODO change path to cluster output folder of repurpose lanes scenario. + make base trips path relative to wd setwd("Y:/net/ils/matsim-mexico-city/case-studies/lane-repurposing/output/output-mexico-city-v1.0-1pct-lane-repurposing") baseTripsFile <- list.files(path = "../../../baseCase/output/output-mexico-city-v1.0-1pct", pattern = "output_trips.csv.gz", full.names = TRUE) From 5f480716309b427bac38e0b6c9376ff3a6db60dd Mon Sep 17 00:00:00 2001 From: simei94 Date: Thu, 28 Mar 2024 19:46:26 -0600 Subject: [PATCH 04/12] create dashboard for lane repurposing analyis --- .../matsim/analysis/BikeLinksAnalysis.java | 154 +++++++++++++++++- .../dashboard/LaneRepurposingDashboard.java | 55 +++++++ .../dashboard/RoadPricingDashboard.java | 4 +- 3 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/matsim/dashboard/LaneRepurposingDashboard.java diff --git a/src/main/java/org/matsim/analysis/BikeLinksAnalysis.java b/src/main/java/org/matsim/analysis/BikeLinksAnalysis.java index a9bec74..9e45c9e 100644 --- a/src/main/java/org/matsim/analysis/BikeLinksAnalysis.java +++ b/src/main/java/org/matsim/analysis/BikeLinksAnalysis.java @@ -20,24 +20,34 @@ import org.matsim.core.api.experimental.events.EventsManager; import org.matsim.core.events.EventsUtils; import org.matsim.core.network.NetworkUtils; +import org.matsim.dashboard.LaneRepurposingDashboard; +import org.matsim.simwrapper.SimWrapper; import org.matsim.vehicles.Vehicle; import picocli.CommandLine; import java.io.File; import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.util.*; +import java.util.stream.Stream; import static org.matsim.application.ApplicationUtils.globFile; -@CommandLine.Command(name = "bike-links", description = "Analyze and check vehicles, which travel on bike links created for the lane repurposing scenario.") +@CommandLine.Command(name = "bike-links", description = "Analyze and check vehicles, which travel on bike links created for the lane repurposing scenario. " + + "This class also creates a dashboard to visualize the data.") public class BikeLinksAnalysis implements MATSimAppCommand, LinkEnterEventHandler, LinkLeaveEventHandler, VehicleEntersTrafficEventHandler, VehicleLeavesTrafficEventHandler { Logger log = LogManager.getLogger(BikeLinksAnalysis.class); @CommandLine.Option(names = "--dir", description = "Path to run directory.") private Path runDir; - @CommandLine.Option(names = "--output", description = "Path to output directory.", defaultValue = "/analysis/repurposeLanes") + @CommandLine.Option(names = "--output", description = "Path to output directory.", defaultValue = "./analysis/repurposeLanes/") private String output; private Map, List>> carsOnBikeLinks = new HashMap<>(); @@ -88,6 +98,19 @@ public Integer call() throws Exception { } } + List travelTimes = new ArrayList<>(); + List travelDists = new ArrayList<>(); + List avgSpeeds = new ArrayList<>(); + List totalTravelTimes = new ArrayList<>(); + List totalTravelDists = new ArrayList<>(); + List totalAvgSpeeds = new ArrayList<>(); + List sharesTravelTimeBikeLinks = new ArrayList<>(); + List sharesTravelDistBikeLinks = new ArrayList<>(); + + + BikeData meanStats = new BikeData(Id.createVehicleId("0"), 0, 0, 0, 0, + 0, 0, 0, 0); + // write bike stats on bike links try (CSVPrinter printer = new CSVPrinter(new FileWriter(analysisDir.getPath() + "/bikes_on_bike_links_stats.csv"), CSVFormat.DEFAULT)) { printer.printRecord("vehicleId", "travelTime [s]", "travelDistance [m]", "avgSpeed [m/s]", "totalTravelTime [s]", @@ -96,12 +119,139 @@ public Integer call() throws Exception { for (BikeData data : this.bikeData) { printer.printRecord(data.bikeId, data.travelTime, data.travelDist, data.avgSpeed, data.totalTravelTime, data.totalTravelDist, data.totalAvgSpeed, data.shareTravelTimeBikeLink, data.shareTravelDistBikeLink); + + meanStats.travelTime += data.travelTime; + meanStats.travelDist += data.travelDist; + meanStats.avgSpeed += data.avgSpeed; + meanStats.totalTravelTime += data.totalTravelTime; + meanStats.totalTravelDist += data.totalTravelDist; + meanStats.totalAvgSpeed += data.totalAvgSpeed; + meanStats.shareTravelTimeBikeLink += data.shareTravelTimeBikeLink; + meanStats.shareTravelDistBikeLink += data.shareTravelDistBikeLink; + + travelTimes.add(data.travelTime); + travelDists.add(data.travelDist); + avgSpeeds.add(data.avgSpeed); + totalTravelTimes.add(data.totalTravelTime); + totalTravelDists.add(data.totalTravelDist); + totalAvgSpeeds.add(data.totalAvgSpeed); + sharesTravelTimeBikeLinks.add(data.shareTravelTimeBikeLink); + sharesTravelDistBikeLinks.add(data.shareTravelDistBikeLink); } } + + meanStats.travelTime = meanStats.travelTime / this.bikeData.size(); + meanStats.travelDist = meanStats.travelDist / this.bikeData.size(); + meanStats.avgSpeed = meanStats.avgSpeed / this.bikeData.size(); + meanStats.totalTravelTime = meanStats.totalTravelTime / this.bikeData.size(); + meanStats.totalTravelDist = meanStats.totalTravelDist / this.bikeData.size(); + meanStats.totalAvgSpeed = meanStats.totalAvgSpeed / this.bikeData.size(); + meanStats.shareTravelTimeBikeLink = meanStats.shareTravelTimeBikeLink / this.bikeData.size(); + meanStats.shareTravelDistBikeLink = meanStats.shareTravelDistBikeLink / this.bikeData.size(); + + + BikeData medianStats = new BikeData(Id.createVehicleId("0"), getMedian(travelTimes), getMedian(travelDists), getMedian(avgSpeeds), getMedian(totalTravelTimes), + getMedian(totalTravelDists), getMedian(totalAvgSpeeds), getMedian(sharesTravelTimeBikeLinks), getMedian(sharesTravelDistBikeLinks)); + + DecimalFormat f = new DecimalFormat("0.00", new DecimalFormatSymbols(Locale.ENGLISH)); + +// write avg share stats + try (CSVPrinter printer = new CSVPrinter(new FileWriter(analysisDir.getPath() + "/avg_share_stats.csv"), CSVFormat.DEFAULT)) { + printer.printRecord("\"mean share of travel time [s] on bike-only links\"", f.format(meanStats.shareTravelTimeBikeLink)); + printer.printRecord("\"median share of travel time [s] on bike-only links\"", f.format(medianStats.shareTravelTimeBikeLink)); + printer.printRecord("\"mean share of travel dist [m] on bike-only links\"", f.format(meanStats.shareTravelDistBikeLink)); + printer.printRecord("\"median share of travel dist [m] on bike-only links\"", f.format(medianStats.shareTravelDistBikeLink)); + printer.printRecord("\"cars traveling on bike-only links\"", this.carsOnBikeLinks.size()); + } + +// write avg stats + try (CSVPrinter printer = new CSVPrinter(new FileWriter(analysisDir.getPath() + "/avg_stats.csv"), CSVFormat.DEFAULT)) { + printer.printRecord("\"mean travel time [s] on bike-only links\"", f.format(meanStats.travelTime)); + printer.printRecord("\"median travel time [s] on bike-only links\"", f.format(medianStats.travelTime)); + printer.printRecord("\"mean travel dist [m] on bike-only links\"", f.format(meanStats.travelDist)); + printer.printRecord("\"median travel dist [m] on bike-only links\"", f.format(medianStats.travelDist)); + printer.printRecord("\"mean travel speed [m/s] on bike-only links\"", f.format(meanStats.avgSpeed)); + printer.printRecord("\"median travel speed [m/s] on bike-only links\"", f.format(medianStats.avgSpeed)); + printer.printRecord("\"mean travel time [s] on all links\"", f.format(meanStats.totalTravelTime)); + printer.printRecord("\"median travel time [s] on all links\"", f.format(medianStats.totalTravelTime)); + printer.printRecord("\"mean travel dist [m] on all links\"", f.format(meanStats.totalTravelDist)); + printer.printRecord("\"median travel dist [m] on all links\"", f.format(medianStats.totalTravelDist)); + printer.printRecord("\"mean travel speed [m/s] on all links\"", f.format(meanStats.totalAvgSpeed)); + printer.printRecord("\"median travel speed [m/s] on all links\"", f.format(medianStats.totalAvgSpeed)); + } + + createDashboard(); + return 0; } + private void createDashboard() throws IOException { + SimWrapper sw = SimWrapper.create(); + + sw.addDashboard(new LaneRepurposingDashboard(output)); + +// the added dashboard will overwrite an existing one, so the following workaround is done +// this only generates the dashboard. If the dashboard includes analysis (like the standard dashboards), SimWrapper.run has to be executed additionally + sw.generate(Path.of(runDir + "/dashboard")); + + String pattern = "*dashboard-*"; + PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern); + + try (Stream fileStream = Files.walk(runDir)) { + // Glob files in the directory matching the pattern + List matchedFiles = fileStream + .filter(Files::isRegularFile) + .filter(path -> matcher.matches(path.getFileName())) + .toList(); + + int i = 0; + for (Path p : matchedFiles) { + int n = Integer.parseInt(p.getFileName().toString().substring(10, 11)); + if (n > i) { + i = n; + } + } + + String newFileName = globFile(runDir, "*dashboard-" + i +"*").getFileName().toString().replace(String.valueOf(i), String.valueOf(i + 1)); + + Files.copy(Path.of(runDir + "/dashboard/dashboard-0.yaml"), Path.of(runDir + "/" + newFileName)); + + try (Stream anotherStream = Files.walk(Path.of(runDir + "/dashboard"))){ + anotherStream + .sorted((p1, p2) -> -p1.compareTo(p2)) + .forEach(path -> { + try { + Files.delete(path); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } catch (IOException f) { + throw new RuntimeException(f); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private Double getMedian(List values) { + + Collections.sort(values); + + int length = values.size(); + // Check if the length of the array is odd or even + if (length % 2 != 0) { + // If odd, return the middle element + return values.get(length / 2); + } else { + // If even, return the average of the two middle elements + int midIndex1 = length / 2 - 1; + int midIndex2 = length / 2; + return (values.get(midIndex1) + values.get(midIndex2)) / 2.0; + } + } + @Override public void handleEvent(LinkLeaveEvent event) { // this event is only needed for getting travel times of agents who travel only one link diff --git a/src/main/java/org/matsim/dashboard/LaneRepurposingDashboard.java b/src/main/java/org/matsim/dashboard/LaneRepurposingDashboard.java new file mode 100644 index 0000000..3d8c71d --- /dev/null +++ b/src/main/java/org/matsim/dashboard/LaneRepurposingDashboard.java @@ -0,0 +1,55 @@ +package org.matsim.dashboard; + +import org.matsim.simwrapper.Dashboard; +import org.matsim.simwrapper.Header; +import org.matsim.simwrapper.Layout; +import org.matsim.simwrapper.viz.*; + +/** + * Shows information about an optional lane repurposing policy case, where lanes for cars are repurposed for bike only. + */ +public class LaneRepurposingDashboard implements Dashboard { + String analysisDir; + String bikeCsv = "bikes_on_bike_links_stats.csv"; + + public LaneRepurposingDashboard(String analysisDir) { + if (analysisDir.startsWith("./")) { + analysisDir = analysisDir.replace("./", ""); + } + this.analysisDir = analysisDir; + } + @Override + public void configure(Header header, Layout layout) { + header.title = "Lane Repurposing"; + header.description = "General information about the simulated lane repurposing policy case. Here, every link (except motorways) " + + "with 2 or more car lanes or more, will be modified such that one of the lanes will be available for bike exclusively."; + + layout.row("first") + .el(Tile.class, (viz, data) -> { + viz.dataset = analysisDir + "avg_share_stats.csv"; + viz.height = 0.1; + }); + + layout.row("second") + .el(Sankey.class, (viz, data) -> { + viz.title = "Modal Shift Sankey Diagram"; + viz.description = "Base case => Lane Repurposing policy case"; + viz.csv = analysisDir + "modalShift.csv"; + viz.height = 2.; + }); + + layout.row("third") + .el(Tile.class, (viz, data) -> { + viz.dataset = analysisDir + "avg_stats.csv"; + viz.height = 0.1; + }); + + layout.row("fourth") + .el(Table.class, (viz, data) -> { + viz.title = "Bike trip statistics"; + viz.description = "for bike-only links and in general"; + viz.dataset = analysisDir + bikeCsv; + viz.showAllRows = false; + }); + } +} diff --git a/src/main/java/org/matsim/dashboard/RoadPricingDashboard.java b/src/main/java/org/matsim/dashboard/RoadPricingDashboard.java index ab697be..708408c 100644 --- a/src/main/java/org/matsim/dashboard/RoadPricingDashboard.java +++ b/src/main/java/org/matsim/dashboard/RoadPricingDashboard.java @@ -15,7 +15,9 @@ public class RoadPricingDashboard implements Dashboard { String share = "share"; - public RoadPricingDashboard() {} + public RoadPricingDashboard() { +// no params needed + } @Override public void configure(Header header, Layout layout) { header.title = "Road Pricing"; From 1c010ceddee3a279a6487787d9762fb921f7fd4a Mon Sep 17 00:00:00 2001 From: simei94 Date: Fri, 29 Mar 2024 16:59:47 -0600 Subject: [PATCH 05/12] modal shift analysis for road pricing area --- src/main/R/analysis/modal_shift_roadPricing.R | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/main/R/analysis/modal_shift_roadPricing.R diff --git a/src/main/R/analysis/modal_shift_roadPricing.R b/src/main/R/analysis/modal_shift_roadPricing.R new file mode 100644 index 0000000..bc6fbb7 --- /dev/null +++ b/src/main/R/analysis/modal_shift_roadPricing.R @@ -0,0 +1,87 @@ +library(tidyverse) +library(matsim) +library(ggalluvial) +library(ggplot2) +library(sf) + +setwd("Y:/net/ils/matsim-mexico-city/case-studies/roadPricing-meta-2050/output/output-mexico-city-v1.0-1pct-roadPricing-meta-2050-fare52") +# setwd("C:/Users/Simon/Desktop/wd/2024-03-25/roadPricingAnalysisTest") +analysisDir <- "analysis/roadpricing" + +crs <- "EPSG:4485" + +shp <- st_read(paste0("./",analysisDir,"/roadPricing_area.shp"), crs=crs) + +baseTripsFile <- list.files(path = "../../../baseCase/output/output-mexico-city-v1.0-1pct", pattern = "output_trips.csv.gz", full.names = TRUE) +roadPricingTripsFile <- list.files(pattern = "output_trips.csv.gz") + +###################################################### policy case trips ##################################################################### + +# not using matsim read trips table function because right now it cannot handle delimiters different from ";" +trips_roadPricing <- read_delim(roadPricingTripsFile, + locale = locale(decimal_mark = "."), + n_max = Inf, + col_types = cols( + start_x = col_character(), + start_y = col_character(), + end_x = col_character(), + end_y = col_character(), + end_link = col_character(), + start_link = col_character() + )) + +trips_roadPricing <- trips_roadPricing %>% + mutate( + start_x = as.double(start_x), + start_y = as.double(start_y), + end_x = as.double(end_x), + end_y = as.double(end_y)) +attr(trips_roadPricing,"table_name") <- roadPricingTripsFile + +points_start <- st_as_sf(trips_roadPricing, coords = c('start_x', 'start_y'), crs = crs) %>% + mutate(start_within = as.integer(st_within(geometry, shp))) + +points_end <- st_as_sf(trips_roadPricing, coords = c('end_x', 'end_y'), crs = crs) %>% + mutate(end_within = as.integer(st_within(geometry, shp))) + +trips_roadPricing <- merge(trips_roadPricing, points_start[, c("trip_id", "start_within")], by="trip_id", all.x=TRUE) %>% + merge(points_end[, c("trip_id", "end_within")], by="trip_id", all.x=TRUE) %>% + filter(!is.na(start_within) | !is.na(end_within)) %>% + select(-geometry.x, -geometry.y, -start_within, -end_within) + +###################################################### base case trips ##################################################################### + +# not using matsim read trips table function because right now it cannot handle delimiters different from ";" +trips_base <- read_delim(baseTripsFile, + locale = locale(decimal_mark = "."), + n_max = Inf, + col_types = cols( + start_x = col_character(), + start_y = col_character(), + end_x = col_character(), + end_y = col_character(), + end_link = col_character(), + start_link = col_character() + )) + +trips_base <- trips_base %>% + mutate( + start_x = as.double(start_x), + start_y = as.double(start_y), + end_x = as.double(end_x), + end_y = as.double(end_y)) +attr(trips_base,"table_name") <- baseTripsFile + +points_start <- st_as_sf(trips_base, coords = c('start_x', 'start_y'), crs = crs) %>% + mutate(start_within = as.integer(st_within(geometry, shp))) + +points_end <- st_as_sf(trips_base, coords = c('end_x', 'end_y'), crs = crs) %>% + mutate(end_within = as.integer(st_within(geometry, shp))) + +trips_base <- merge(trips_base, points_start[, c("trip_id", "start_within")], by="trip_id", all.x=FALSE, all.y=FALSE) %>% + merge(points_end[, c("trip_id", "end_within")], by="trip_id", all.x=TRUE) %>% + filter(!is.na(start_within) | !is.na(end_within)) %>% + select(-geometry.x, -geometry.y, -start_within, -end_within) + +plotModalShiftSankey(trips_base, trips_roadPricing, dump.output.to = getwd()) +write.csv(trips_roadPricing, file=paste0(getwd(), analysisDir, "/output_trips.roadPricing-area.csv.gz"), quote=FALSE) From 2fe2127e2d4b7f4a9b44f57188df7fbc0b7ef711 Mon Sep 17 00:00:00 2001 From: simei94 Date: Mon, 1 Apr 2024 17:48:10 -0600 Subject: [PATCH 06/12] add advanced road pricing dashboard --- .../matsim/analysis/BikeLinksAnalysis.java | 58 +-- .../roadpricing/MexicoCityTripAnalysis.java | 445 ++++++++++++++++++ .../AdvancedRoadPricingDashboard.java | 143 ++++++ .../dashboard/LaneRepurposingDashboard.java | 6 +- .../org/matsim/prepare/MexicoCityUtils.java | 64 +++ 5 files changed, 659 insertions(+), 57 deletions(-) create mode 100644 src/main/java/org/matsim/analysis/roadpricing/MexicoCityTripAnalysis.java create mode 100644 src/main/java/org/matsim/dashboard/AdvancedRoadPricingDashboard.java diff --git a/src/main/java/org/matsim/analysis/BikeLinksAnalysis.java b/src/main/java/org/matsim/analysis/BikeLinksAnalysis.java index 9e45c9e..30144e7 100644 --- a/src/main/java/org/matsim/analysis/BikeLinksAnalysis.java +++ b/src/main/java/org/matsim/analysis/BikeLinksAnalysis.java @@ -21,21 +21,16 @@ import org.matsim.core.events.EventsUtils; import org.matsim.core.network.NetworkUtils; import org.matsim.dashboard.LaneRepurposingDashboard; -import org.matsim.simwrapper.SimWrapper; +import org.matsim.prepare.MexicoCityUtils; import org.matsim.vehicles.Vehicle; import picocli.CommandLine; import java.io.File; import java.io.FileWriter; -import java.io.IOException; -import java.nio.file.FileSystems; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.PathMatcher; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.*; -import java.util.stream.Stream; import static org.matsim.application.ApplicationUtils.globFile; @@ -181,60 +176,11 @@ public Integer call() throws Exception { printer.printRecord("\"median travel speed [m/s] on all links\"", f.format(medianStats.totalAvgSpeed)); } - createDashboard(); + MexicoCityUtils.addDashboardToExistingRunOutput(new LaneRepurposingDashboard(output), runDir); return 0; } - private void createDashboard() throws IOException { - SimWrapper sw = SimWrapper.create(); - - sw.addDashboard(new LaneRepurposingDashboard(output)); - -// the added dashboard will overwrite an existing one, so the following workaround is done -// this only generates the dashboard. If the dashboard includes analysis (like the standard dashboards), SimWrapper.run has to be executed additionally - sw.generate(Path.of(runDir + "/dashboard")); - - String pattern = "*dashboard-*"; - PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern); - - try (Stream fileStream = Files.walk(runDir)) { - // Glob files in the directory matching the pattern - List matchedFiles = fileStream - .filter(Files::isRegularFile) - .filter(path -> matcher.matches(path.getFileName())) - .toList(); - - int i = 0; - for (Path p : matchedFiles) { - int n = Integer.parseInt(p.getFileName().toString().substring(10, 11)); - if (n > i) { - i = n; - } - } - - String newFileName = globFile(runDir, "*dashboard-" + i +"*").getFileName().toString().replace(String.valueOf(i), String.valueOf(i + 1)); - - Files.copy(Path.of(runDir + "/dashboard/dashboard-0.yaml"), Path.of(runDir + "/" + newFileName)); - - try (Stream anotherStream = Files.walk(Path.of(runDir + "/dashboard"))){ - anotherStream - .sorted((p1, p2) -> -p1.compareTo(p2)) - .forEach(path -> { - try { - Files.delete(path); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } catch (IOException f) { - throw new RuntimeException(f); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - private Double getMedian(List values) { Collections.sort(values); diff --git a/src/main/java/org/matsim/analysis/roadpricing/MexicoCityTripAnalysis.java b/src/main/java/org/matsim/analysis/roadpricing/MexicoCityTripAnalysis.java new file mode 100644 index 0000000..2159315 --- /dev/null +++ b/src/main/java/org/matsim/analysis/roadpricing/MexicoCityTripAnalysis.java @@ -0,0 +1,445 @@ +package org.matsim.analysis.roadpricing; + +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2LongMap; +import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.matsim.application.CommandSpec; +import org.matsim.application.MATSimAppCommand; +import org.matsim.application.options.CsvOptions; +import org.matsim.application.options.InputOptions; +import org.matsim.application.options.OutputOptions; +import org.matsim.application.options.ShpOptions; +import org.matsim.core.utils.io.IOUtils; +import org.matsim.dashboard.AdvancedRoadPricingDashboard; +import org.matsim.prepare.MexicoCityUtils; +import picocli.CommandLine; +import tech.tablesaw.api.*; +import tech.tablesaw.io.csv.CsvReadOptions; +import tech.tablesaw.joining.DataFrameJoiner; +import tech.tablesaw.selection.Selection; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +import static tech.tablesaw.aggregate.AggregateFunctions.count; + +/** + * class copied and enhanced from matsim-libs org.matsim.application.analysis.population.TripAnalysis + */ +@CommandLine.Command(name = "trips", description = "Calculates various trip related metrics.") +@CommandSpec( + requires = {"trips.csv", "persons.csv"}, + produces = {"mode_share.csv", "mode_share_per_dist.csv", "mode_users.csv", "trip_stats.csv", "population_trip_stats.csv", "trip_purposes_by_hour.csv"} +) +public class MexicoCityTripAnalysis implements MATSimAppCommand { + + private static final Logger log = LogManager.getLogger(MexicoCityTripAnalysis.class); + + @CommandLine.Mixin + private InputOptions input = InputOptions.ofCommand(MexicoCityTripAnalysis.class); + @CommandLine.Mixin + private OutputOptions output = OutputOptions.ofCommand(MexicoCityTripAnalysis.class); + + @CommandLine.Option(names = "--match-id", description = "Pattern to filter agents by id") + private String matchId; + + @CommandLine.Option(names = "--dist-groups", split = ",", description = "List of distances for binning", defaultValue = "0,1000,2000,5000,10000,20000") + private List distGroups; + + @CommandLine.Option(names = "--modes", split = ",", description = "List of considered modes, if not set all will be used") + private List modeOrder; + + @CommandLine.Option(names = "--shp-filter", description = "Define how the shp file filtering should work", defaultValue = "home") + private LocationFilter filter; + + @CommandLine.Mixin + private ShpOptions shp; + + public static void main(String[] args) { + new MexicoCityTripAnalysis().execute(args); + } + + private static String cut(long dist, List distGroups, List labels) { + + int idx = Collections.binarySearch(distGroups, dist); + + if (idx >= 0) + return labels.get(idx); + + int ins = -(idx + 1); + return labels.get(ins - 1); + } + + private static int[] durationToHour(String d) { + return Arrays.stream(d.split(":")).mapToInt(Integer::valueOf).toArray(); + } + + private static int durationToSeconds(String d) { + String[] split = d.split(":"); + return (Integer.parseInt(split[0]) * 60 * 60) + (Integer.parseInt(split[1]) * 60) + Integer.parseInt(split[2]); + } + + @Override + public Integer call() throws Exception { + + Table persons = Table.read().csv(CsvReadOptions.builder(IOUtils.getBufferedReader(input.getPath("persons.csv"))) + .columnTypesPartial(Map.of("person", ColumnType.TEXT)) + .sample(false) + .separator(new CsvOptions().detectDelimiter(input.getPath("persons.csv"))).build()); + + int total = persons.rowCount(); + + if (matchId != null) { + log.info("Using id filter {}", matchId); + persons = persons.where(persons.textColumn("person").matchesRegex(matchId)); + } + + // Home filter by standard attribute + if (shp.isDefined() && filter == LocationFilter.HOME) { + Geometry geometry = shp.getGeometry(); + GeometryFactory f = new GeometryFactory(); + + IntList idx = new IntArrayList(); + + for (int i = 0; i < persons.rowCount(); i++) { + Row row = persons.row(i); + Point p = f.createPoint(new Coordinate(row.getDouble("home_x"), row.getDouble("home_y"))); + if (geometry.contains(p)) { + idx.add(i); + } + } + + persons = persons.where(Selection.with(idx.toIntArray())); + } + + log.info("Filtered {} out of {} persons", persons.rowCount(), total); + + Map columnTypes = new HashMap<>(Map.of("person", ColumnType.TEXT, + "trav_time", ColumnType.STRING, "wait_time", ColumnType.STRING, "dep_time", ColumnType.STRING, + "longest_distance_mode", ColumnType.STRING, "main_mode", ColumnType.STRING, + "start_activity_type", ColumnType.TEXT, "end_activity_type", ColumnType.TEXT, + "first_pt_boarding_stop", ColumnType.TEXT, "last_pt_egress_stop", ColumnType.TEXT)); + + // Map.of only has 10 argument max + columnTypes.put("traveled_distance", ColumnType.LONG); + + Table trips = Table.read().csv(CsvReadOptions.builder(IOUtils.getBufferedReader(input.getPath("trips.csv"))) + .columnTypesPartial(columnTypes) + .sample(false) + .separator(CsvOptions.detectDelimiter(input.getPath("trips.csv"))).build()); + + // Trip filter with start AND end + if (shp.isDefined() && filter == LocationFilter.TRIP_START_AND_END) { + Geometry geometry = shp.getGeometry(); + GeometryFactory f = new GeometryFactory(); + IntList idx = new IntArrayList(); + + for (int i = 0; i < trips.rowCount(); i++) { + Row row = trips.row(i); + Point start = f.createPoint(new Coordinate(row.getDouble("start_x"), row.getDouble("start_y"))); + Point end = f.createPoint(new Coordinate(row.getDouble("end_x"), row.getDouble("end_y"))); + if (geometry.contains(start) && geometry.contains(end)) { + idx.add(i); + } + } + + trips = trips.where(Selection.with(idx.toIntArray())); +// trip filter with start OR end + } else if (shp.isDefined() && filter == LocationFilter.TRIP_START_OR_END) { + Geometry geometry = shp.getGeometry(); + GeometryFactory f = new GeometryFactory(); + IntList idx = new IntArrayList(); + + for (int i = 0; i < trips.rowCount(); i++) { + Row row = trips.row(i); + Point start = f.createPoint(new Coordinate(row.getDouble("start_x"), row.getDouble("start_y"))); + Point end = f.createPoint(new Coordinate(row.getDouble("end_x"), row.getDouble("end_y"))); + if (geometry.contains(start) || geometry.contains(end)) { + idx.add(i); + } + } + + log.info(trips.rowCount()); + trips = trips.where(Selection.with(idx.toIntArray())); + log.info(trips.rowCount()); + } + + // Use longest_distance_mode where main_mode is not present + trips.stringColumn("main_mode") + .set(trips.stringColumn("main_mode").isMissing(), + trips.stringColumn("longest_distance_mode")); + + + Table joined = new DataFrameJoiner(trips, "person").inner(persons); + + log.info("Filtered {} out of {} trips", joined.rowCount(), trips.rowCount()); + + List labels = new ArrayList<>(); + for (int i = 0; i < distGroups.size() - 1; i++) { + labels.add(String.format("%d - %d", distGroups.get(i), distGroups.get(i + 1))); + } + labels.add(distGroups.get(distGroups.size() - 1) + "+"); + distGroups.add(Long.MAX_VALUE); + + StringColumn dist_group = joined.longColumn("traveled_distance") + .map(dist -> cut(dist, distGroups, labels), ColumnType.STRING::create).setName("dist_group"); + + joined.addColumns(dist_group); + + writeModeShare(joined, labels); + + writePopulationStats(persons, joined); + + writeTripStats(joined); + + writeTripPurposes(joined); + +// TODO check if the paths work here + String analysisDir = output.getPath().toString().substring(0, output.getPath().toString().lastIndexOf("\\")); + Path runDir = Path.of(input.getPath().substring(0, input.getPath().lastIndexOf("\\"))); + + if (Path.of(analysisDir).isAbsolute()) { + analysisDir = runDir.relativize(Path.of(analysisDir)).toString(); + } + + MexicoCityUtils.addDashboardToExistingRunOutput(new AdvancedRoadPricingDashboard(analysisDir + "\\"), + runDir); + + return 0; + } + + private void writeModeShare(Table trips, List labels) { + + Table aggr = trips.summarize("trip_id", count).by("dist_group", "main_mode"); + + DoubleColumn share = aggr.numberColumn(2).divide(aggr.numberColumn(2).sum()).setName("share"); + aggr.replaceColumn(2, share); + + // Sort by dist_group and mode + Comparator cmp = Comparator.comparingInt(row -> labels.indexOf(row.getString("dist_group"))); + aggr = aggr.sortOn(cmp.thenComparing(row -> row.getString("main_mode"))); + + aggr.write().csv(output.getPath("mode_share.csv").toFile()); + + // Norm each dist_group to 1 + for (String label : labels) { + DoubleColumn dist_group = aggr.doubleColumn("share"); + Selection sel = aggr.stringColumn("dist_group").isEqualTo(label); + + double total = dist_group.where(sel).sum(); + if (total > 0) + dist_group.set(sel, dist_group.divide(total)); + } + + aggr.write().csv(output.getPath("mode_share_per_dist.csv").toFile()); + + // Derive mode order if not given + if (modeOrder == null) { + modeOrder = new ArrayList<>(); + for (Row row : aggr) { + String mainMode = row.getString("main_mode"); + if (!modeOrder.contains(mainMode)) { + modeOrder.add(mainMode); + } + } + } + } + + private void writeTripStats(Table trips) throws IOException { + + // Stats per mode + Object2IntMap n = new Object2IntLinkedOpenHashMap<>(); + Object2LongMap travelTime = new Object2LongOpenHashMap<>(); + Object2LongMap travelDistance = new Object2LongOpenHashMap<>(); + + for (Row trip : trips) { + String mainMode = trip.getString("main_mode"); + + n.mergeInt(mainMode, 1, Integer::sum); + travelTime.mergeLong(mainMode, durationToSeconds(trip.getString("trav_time")), Long::sum); + travelDistance.mergeLong(mainMode, trip.getLong("traveled_distance"), Long::sum); + } + + try (CSVPrinter printer = new CSVPrinter(Files.newBufferedWriter(output.getPath("trip_stats.csv")), CSVFormat.DEFAULT)) { + + printer.print("Info"); + for (String m : modeOrder) { + printer.print(m); + } + printer.println(); + + printer.print("Number of trips"); + for (String m : modeOrder) { + printer.print(n.getInt(m)); + } + printer.println(); + + printer.print("Total time traveled [h]"); + for (String m : modeOrder) { + long seconds = travelTime.getLong(m); + printer.print(new BigDecimal(seconds / (60d * 60d)).setScale(0, RoundingMode.HALF_UP)); + } + printer.println(); + + printer.print("Total distance traveled [km]"); + for (String m : modeOrder) { + double meter = travelDistance.getLong(m); + printer.print(new BigDecimal(meter / 1000d).setScale(0, RoundingMode.HALF_UP)); + } + printer.println(); + + printer.print("Avg. speed [km/h]"); + for (String m : modeOrder) { + double speed = (travelDistance.getLong(m) / 1000d) / (travelTime.getLong(m) / (60d * 60d)); + printer.print(new BigDecimal(speed).setScale(2, RoundingMode.HALF_UP)); + + } + printer.println(); + + printer.print("Avg. distance per trip [km]"); + for (String m : modeOrder) { + double avg = (travelDistance.getLong(m) / 1000d) / (n.getInt(m)); + printer.print(new BigDecimal(avg).setScale(2, RoundingMode.HALF_UP)); + + } + printer.println(); + } + } + + private void writePopulationStats(Table persons, Table trips) throws IOException { + + Object2IntMap tripsPerPerson = new Object2IntLinkedOpenHashMap<>(); + Map> modesPerPerson = new LinkedHashMap<>(); + + for (Row trip : trips) { + String id = trip.getString("person"); + tripsPerPerson.mergeInt(id, 1, Integer::sum); + String mode = trip.getString("main_mode"); + modesPerPerson.computeIfAbsent(id, s -> new LinkedHashSet<>()).add(mode); + } + + Object2IntMap usedModes = new Object2IntLinkedOpenHashMap<>(); + for (Map.Entry> e : modesPerPerson.entrySet()) { + for (String mode : e.getValue()) { + usedModes.mergeInt(mode, 1, Integer::sum); + } + } + + double totalMobile = tripsPerPerson.size(); + double avgTripsMobile = tripsPerPerson.values().intStream().average().orElse(0); + + for (Row person : persons) { + String id = person.getString("person"); + if (!tripsPerPerson.containsKey(id)) + tripsPerPerson.put(id, 0); + } + + double avgTrips = tripsPerPerson.values().intStream().average().orElse(0); + + Table table = Table.create(TextColumn.create("main_mode", usedModes.size()), DoubleColumn.create("user", usedModes.size())); + + int i = 0; + for (String m : modeOrder) { + int n = usedModes.getInt(m); + table.textColumn(0).set(i, m); + table.doubleColumn(1).set(i++, n / totalMobile); + } + + table.write().csv(output.getPath("mode_users.csv").toFile()); + + try (CSVPrinter printer = new CSVPrinter(Files.newBufferedWriter(output.getPath("population_trip_stats.csv")), CSVFormat.DEFAULT)) { + + printer.printRecord("Info", "Value"); + printer.printRecord("Persons", tripsPerPerson.size()); + printer.printRecord("Mobile persons [%]", new BigDecimal(100 * totalMobile / tripsPerPerson.size()).setScale(2, RoundingMode.HALF_UP)); + printer.printRecord("Avg. trips", new BigDecimal(avgTrips).setScale(2, RoundingMode.HALF_UP)); + printer.printRecord("Avg. trip per mobile persons", new BigDecimal(avgTripsMobile).setScale(2, RoundingMode.HALF_UP)); + } + } + + private void writeTripPurposes(Table trips) { + + IntList departure = new IntArrayList(trips.rowCount()); + IntList arrival = new IntArrayList(trips.rowCount()); + + for (Row t : trips) { + int[] depTime = durationToHour(t.getString("dep_time")); + departure.add(depTime[0]); + + int[] travTimes = durationToHour(t.getString("trav_time")); + + depTime[2] += travTimes[2]; + if (depTime[2] >= 60) + depTime[1]++; + + depTime[1] += travTimes[1]; + if (depTime[1] >= 60) + depTime[0]++; + + depTime[0] += travTimes[0]; + + arrival.add(depTime[0]); + } + + trips.addColumns( + IntColumn.create("departure_h", departure.intStream().toArray()), + IntColumn.create("arrival_h", arrival.intStream().toArray()) + ); + + TextColumn purpose = trips.textColumn("end_activity_type"); + + // Remove suffix durations like _345 + Selection withDuration = purpose.matchesRegex("^.+_[0-9]+$"); + purpose.set(withDuration, purpose.where(withDuration).replaceAll("_[0-9]+$", "")); + + Table tArrival = trips.summarize("trip_id", count).by("end_activity_type", "arrival_h"); + + tArrival.column(0).setName("purpose"); + tArrival.column(1).setName("h"); + + DoubleColumn share = tArrival.numberColumn(2).divide(tArrival.numberColumn(2).sum()).setName("arrival"); + tArrival.replaceColumn(2, share); + + Table tDeparture = trips.summarize("trip_id", count).by("end_activity_type", "departure_h"); + + tDeparture.column(0).setName("purpose"); + tDeparture.column(1).setName("h"); + + share = tDeparture.numberColumn(2).divide(tDeparture.numberColumn(2).sum()).setName("departure"); + tDeparture.replaceColumn(2, share); + + + Table table = new DataFrameJoiner(tArrival, "purpose", "h").fullOuter(tDeparture).sortOn(0, 1); + + table.doubleColumn("departure").setMissingTo(0.0); + table.doubleColumn("arrival").setMissingTo(0.0); + + table.write().csv(output.getPath("trip_purposes_by_hour.csv").toFile()); + + } + + /** + * How shape file filtering should be applied. + */ + enum LocationFilter { + TRIP_START_AND_END, + TRIP_START_OR_END, + HOME, + NONE + } +} diff --git a/src/main/java/org/matsim/dashboard/AdvancedRoadPricingDashboard.java b/src/main/java/org/matsim/dashboard/AdvancedRoadPricingDashboard.java new file mode 100644 index 0000000..a9dc2ec --- /dev/null +++ b/src/main/java/org/matsim/dashboard/AdvancedRoadPricingDashboard.java @@ -0,0 +1,143 @@ +package org.matsim.dashboard; + +import org.matsim.simwrapper.Dashboard; +import org.matsim.simwrapper.Header; +import org.matsim.simwrapper.Layout; +import org.matsim.simwrapper.viz.ColorScheme; +import org.matsim.simwrapper.viz.Plotly; +import org.matsim.simwrapper.viz.Sankey; +import org.matsim.simwrapper.viz.Table; +import tech.tablesaw.plotly.components.Axis; +import tech.tablesaw.plotly.traces.BarTrace; + +import java.util.List; +import java.util.Map; + +/** + * Shows advanced information about an optional road pricing policy case, where lanes for cars are repurposed for bike only. + */ +public class AdvancedRoadPricingDashboard implements Dashboard { + + String analysisDir; + private static final String SOURCE = "source"; + private static final String MAIN_MODE = "main_mode"; + private static final String SHARE = "share"; + + public AdvancedRoadPricingDashboard(String analysisDir) { + if (analysisDir.startsWith("./")) { + analysisDir = analysisDir.replace("./", ""); + } + this.analysisDir = analysisDir; + } + + @Override + public void configure(Header header, Layout layout) { + + this.analysisDir = analysisDir.replace('\\', '/'); + + header.title = "Road Pricing - advanced"; + header.description = "Advanced information about the simulated road pricing policy case."; + + layout.row("first") + .el(Plotly.class, (viz, data) -> { + viz.title = "Modal split"; + viz.description = "of road pricing area"; + + viz.layout = tech.tablesaw.plotly.components.Layout.builder() + .barMode(tech.tablesaw.plotly.components.Layout.BarMode.STACK) + .build(); + + Plotly.DataSet ds = viz.addDataset(analysisDir + "mode_share.csv") + .constant(SOURCE, "RoadPricing Case") + .aggregate(List.of(MAIN_MODE), SHARE, Plotly.AggrFunc.SUM); + + + viz.addDataset("../../baseCaseRoadPricing/mode_share.csv") + .constant(SOURCE, "Base Case") + .aggregate(List.of(MAIN_MODE), SHARE, Plotly.AggrFunc.SUM); + + viz.mergeDatasets = true; + viz.addTrace(BarTrace.builder(Plotly.OBJ_INPUT, Plotly.INPUT).orientation(BarTrace.Orientation.HORIZONTAL).build(), + ds.mapping() + .name(MAIN_MODE) + .y(SOURCE) + .x(SHARE) + ); + }); + + layout.row("second") + .el(Sankey.class, (viz, data) -> { + viz.title = "Modal Shift Sankey Diagram"; + viz.description = "Base case => Road Pricing policy case"; + viz.csv = analysisDir + "modalShift.csv"; + }) + .el(Table.class, (viz, data) -> { + viz.title = "Mode Statistics"; + viz.description = "by main mode, over whole trip (including access & egress)"; + viz.dataset = analysisDir + "trip_stats.csv"; + viz.showAllRows = true; + }); + + layout.row("third") + .el(Plotly.class, (viz, data) -> { + viz.title = "Mode usage"; + viz.description = "Share of persons using a main mode at least once per day."; + + Plotly.DataSet ds = viz.addDataset(analysisDir + "mode_users.csv"); + viz.addTrace(BarTrace.builder(Plotly.OBJ_INPUT, Plotly.INPUT).build(), ds.mapping() + .x(MAIN_MODE) + .y("user") + .name(MAIN_MODE) + ); + + ds.constant(SOURCE, "RoadPricing Case"); + + viz.addDataset("../../baseCaseRoadPricing/mode_users.csv") + .constant(SOURCE, "Base Case"); + + viz.multiIndex = Map.of(MAIN_MODE, SOURCE); + viz.mergeDatasets = true; + }); + + layout.row("departures") + .el(Plotly.class, (viz, data) -> { + viz.title = "Departures"; + viz.description = "by hour and purpose"; + viz.layout = tech.tablesaw.plotly.components.Layout.builder() + .xAxis(Axis.builder().title("Hour").build()) + .yAxis(Axis.builder().title(SHARE).build()) + .barMode(tech.tablesaw.plotly.components.Layout.BarMode.STACK) + .build(); + + viz.addTrace(BarTrace.builder(Plotly.OBJ_INPUT, Plotly.INPUT).build(), + viz.addDataset(analysisDir + "trip_purposes_by_hour.csv").mapping() + .name("purpose", ColorScheme.Spectral) + .x("h") + .y("departure") + ); + }); + + layout.row("arrivals") + .el(Plotly.class, (viz, data) -> { + viz.title = "Arrivals"; + viz.description = "by hour and purpose"; + viz.layout = tech.tablesaw.plotly.components.Layout.builder() + .xAxis(Axis.builder().title("Hour").build()) + .yAxis(Axis.builder().title(SHARE).build()) + .barMode(tech.tablesaw.plotly.components.Layout.BarMode.STACK) + .build(); + + viz.addTrace(BarTrace.builder(Plotly.OBJ_INPUT, Plotly.INPUT).build(), + viz.addDataset(analysisDir + "trip_purposes_by_hour.csv").mapping() + .name("purpose", ColorScheme.Spectral) + .x("h") + .y("arrival") + ); + }); + } + + @Override + public double priority() { + return -3; + } +} diff --git a/src/main/java/org/matsim/dashboard/LaneRepurposingDashboard.java b/src/main/java/org/matsim/dashboard/LaneRepurposingDashboard.java index 3d8c71d..2a54eb6 100644 --- a/src/main/java/org/matsim/dashboard/LaneRepurposingDashboard.java +++ b/src/main/java/org/matsim/dashboard/LaneRepurposingDashboard.java @@ -35,7 +35,6 @@ public void configure(Header header, Layout layout) { viz.title = "Modal Shift Sankey Diagram"; viz.description = "Base case => Lane Repurposing policy case"; viz.csv = analysisDir + "modalShift.csv"; - viz.height = 2.; }); layout.row("third") @@ -52,4 +51,9 @@ public void configure(Header header, Layout layout) { viz.showAllRows = false; }); } + + @Override + public double priority() { + return -3; + } } diff --git a/src/main/java/org/matsim/prepare/MexicoCityUtils.java b/src/main/java/org/matsim/prepare/MexicoCityUtils.java index 0586065..1541eb7 100644 --- a/src/main/java/org/matsim/prepare/MexicoCityUtils.java +++ b/src/main/java/org/matsim/prepare/MexicoCityUtils.java @@ -4,11 +4,21 @@ import org.matsim.api.core.v01.Id; import org.matsim.api.core.v01.network.Link; import org.matsim.api.core.v01.population.Person; +import org.matsim.simwrapper.Dashboard; +import org.matsim.simwrapper.SimWrapper; +import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; +import java.nio.file.FileSystems; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.util.List; import java.util.Objects; +import java.util.stream.Stream; + +import static org.matsim.application.ApplicationUtils.globFile; /** * Scenario related utils class. @@ -88,5 +98,59 @@ public static Coord roundCoord(Coord coord) { return new Coord(roundNumber(coord.getX()), roundNumber(coord.getY())); } + public static void addDashboardToExistingRunOutput(Dashboard dashboard, Path runDir) throws IOException { + SimWrapper sw = SimWrapper.create(); + + sw.addDashboard(dashboard); + +// the added dashboard will overwrite an existing one, so the following workaround is done +// this only generates the dashboard. If the dashboard includes analysis (like the standard dashboards), SimWrapper.run has to be executed additionally + sw.generate(Path.of(runDir + "/dashboard")); + + String pattern = "*dashboard-*"; + PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern); + String newFileName; + + try (Stream fileStream = Files.walk(runDir, 1)) { + // Glob files in the directory matching the pattern + List matchedFiles = fileStream + .filter(Files::isRegularFile) + .filter(path -> matcher.matches(path.getFileName())) + .toList(); + + int i = 0; + for (Path p : matchedFiles) { + int n = Integer.parseInt(p.getFileName().toString().substring(10, 11)); + if (n > i) { + i = n; + } + } + + if (matchedFiles.isEmpty()) { + newFileName = "dashboard-0.yaml"; + } else { + newFileName = globFile(runDir, "*dashboard-" + i +"*").getFileName().toString().replace(String.valueOf(i), String.valueOf(i + 1)); + } + + Files.copy(Path.of(runDir + "/dashboard/dashboard-0.yaml"), Path.of(runDir + "/" + newFileName)); + + try (Stream anotherStream = Files.walk(Path.of(runDir + "/dashboard"))){ + anotherStream + .sorted((p1, p2) -> -p1.compareTo(p2)) + .forEach(path -> { + try { + Files.delete(path); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } catch (IOException f) { + throw new RuntimeException(f); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } From 01c41c808d8f23dddec42ae6d49e4bd42e327a38 Mon Sep 17 00:00:00 2001 From: simei94 Date: Mon, 1 Apr 2024 17:53:55 -0600 Subject: [PATCH 07/12] checkstyle --- .../org/matsim/analysis/roadpricing/MexicoCityTripAnalysis.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/matsim/analysis/roadpricing/MexicoCityTripAnalysis.java b/src/main/java/org/matsim/analysis/roadpricing/MexicoCityTripAnalysis.java index 2159315..6a6e2ca 100644 --- a/src/main/java/org/matsim/analysis/roadpricing/MexicoCityTripAnalysis.java +++ b/src/main/java/org/matsim/analysis/roadpricing/MexicoCityTripAnalysis.java @@ -39,7 +39,7 @@ import static tech.tablesaw.aggregate.AggregateFunctions.count; /** - * class copied and enhanced from matsim-libs org.matsim.application.analysis.population.TripAnalysis + * class copied and enhanced from matsim-libs org.matsim.application.analysis.population.TripAnalysis. */ @CommandLine.Command(name = "trips", description = "Calculates various trip related metrics.") @CommandSpec( From 5a6c4a2b579d564a245fd1ec4c554c7822a28f00 Mon Sep 17 00:00:00 2001 From: simei94 Date: Mon, 1 Apr 2024 18:13:11 -0600 Subject: [PATCH 08/12] simplify input --- .../roadpricing/MexicoCityTripAnalysis.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/matsim/analysis/roadpricing/MexicoCityTripAnalysis.java b/src/main/java/org/matsim/analysis/roadpricing/MexicoCityTripAnalysis.java index 6a6e2ca..095056d 100644 --- a/src/main/java/org/matsim/analysis/roadpricing/MexicoCityTripAnalysis.java +++ b/src/main/java/org/matsim/analysis/roadpricing/MexicoCityTripAnalysis.java @@ -36,6 +36,7 @@ import java.nio.file.Path; import java.util.*; +import static org.matsim.application.ApplicationUtils.globFile; import static tech.tablesaw.aggregate.AggregateFunctions.count; /** @@ -43,7 +44,7 @@ */ @CommandLine.Command(name = "trips", description = "Calculates various trip related metrics.") @CommandSpec( - requires = {"trips.csv", "persons.csv"}, + requires = {"path"}, produces = {"mode_share.csv", "mode_share_per_dist.csv", "mode_users.csv", "trip_stats.csv", "population_trip_stats.csv", "trip_purposes_by_hour.csv"} ) public class MexicoCityTripAnalysis implements MATSimAppCommand { @@ -97,10 +98,13 @@ private static int durationToSeconds(String d) { @Override public Integer call() throws Exception { - Table persons = Table.read().csv(CsvReadOptions.builder(IOUtils.getBufferedReader(input.getPath("persons.csv"))) + String tripsPath = globFile(Path.of(input.getPath()), "*output_trips.*").toString(); + String personsPath = globFile(Path.of(input.getPath()), "*output_persons.*").toString(); + + Table persons = Table.read().csv(CsvReadOptions.builder(IOUtils.getBufferedReader(personsPath)) .columnTypesPartial(Map.of("person", ColumnType.TEXT)) .sample(false) - .separator(new CsvOptions().detectDelimiter(input.getPath("persons.csv"))).build()); + .separator(new CsvOptions().detectDelimiter(personsPath)).build()); int total = persons.rowCount(); @@ -138,10 +142,10 @@ public Integer call() throws Exception { // Map.of only has 10 argument max columnTypes.put("traveled_distance", ColumnType.LONG); - Table trips = Table.read().csv(CsvReadOptions.builder(IOUtils.getBufferedReader(input.getPath("trips.csv"))) + Table trips = Table.read().csv(CsvReadOptions.builder(IOUtils.getBufferedReader(tripsPath)) .columnTypesPartial(columnTypes) .sample(false) - .separator(CsvOptions.detectDelimiter(input.getPath("trips.csv"))).build()); + .separator(CsvOptions.detectDelimiter(tripsPath)).build()); // Trip filter with start AND end if (shp.isDefined() && filter == LocationFilter.TRIP_START_AND_END) { @@ -209,9 +213,8 @@ public Integer call() throws Exception { writeTripPurposes(joined); -// TODO check if the paths work here String analysisDir = output.getPath().toString().substring(0, output.getPath().toString().lastIndexOf("\\")); - Path runDir = Path.of(input.getPath().substring(0, input.getPath().lastIndexOf("\\"))); + Path runDir = Path.of(input.getPath()); if (Path.of(analysisDir).isAbsolute()) { analysisDir = runDir.relativize(Path.of(analysisDir)).toString(); From 76914898c97e2075d4976ed1862a991d19106096 Mon Sep 17 00:00:00 2001 From: simei94 Date: Wed, 3 Apr 2024 14:13:43 -0600 Subject: [PATCH 09/12] flexible input path --- .../analysis/roadpricing/MexicoCityTripAnalysis.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/matsim/analysis/roadpricing/MexicoCityTripAnalysis.java b/src/main/java/org/matsim/analysis/roadpricing/MexicoCityTripAnalysis.java index 095056d..ad4a905 100644 --- a/src/main/java/org/matsim/analysis/roadpricing/MexicoCityTripAnalysis.java +++ b/src/main/java/org/matsim/analysis/roadpricing/MexicoCityTripAnalysis.java @@ -213,7 +213,14 @@ public Integer call() throws Exception { writeTripPurposes(joined); - String analysisDir = output.getPath().toString().substring(0, output.getPath().toString().lastIndexOf("\\")); + String analysisDir = ""; + + if (output.getPath().toString().contains("\\")) { + analysisDir = output.getPath().toString().substring(0, output.getPath().toString().lastIndexOf("\\")); + } else if (output.getPath().toString().contains("/")) { + analysisDir = output.getPath().toString().substring(0, output.getPath().toString().lastIndexOf("/")); + } + Path runDir = Path.of(input.getPath()); if (Path.of(analysisDir).isAbsolute()) { From 29d7633daa2a064b7d4c9ae128637083929cab0a Mon Sep 17 00:00:00 2001 From: simei94 Date: Wed, 3 Apr 2024 16:42:47 -0600 Subject: [PATCH 10/12] add optparse --- src/main/R/analysis/modal_shift_roadPricing.R | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/R/analysis/modal_shift_roadPricing.R b/src/main/R/analysis/modal_shift_roadPricing.R index bc6fbb7..ffa14e5 100644 --- a/src/main/R/analysis/modal_shift_roadPricing.R +++ b/src/main/R/analysis/modal_shift_roadPricing.R @@ -3,9 +3,23 @@ library(matsim) library(ggalluvial) library(ggplot2) library(sf) +library(optparse) -setwd("Y:/net/ils/matsim-mexico-city/case-studies/roadPricing-meta-2050/output/output-mexico-city-v1.0-1pct-roadPricing-meta-2050-fare52") -# setwd("C:/Users/Simon/Desktop/wd/2024-03-25/roadPricingAnalysisTest") +option_list <- list( + make_option(c("-d", "--runDir"), type="character", default=NULL, + help="Path of run directory. Avoid using '/', use '/' instead", metavar="character")) + +opt_parser <- OptionParser(option_list=option_list) +opt <- parse_args(opt_parser) + +# if you want to run code without run args -> comment out the following if condition + setwd() manually +if (is.null(opt$runDir)){ + print_help(opt_parser) + stop("At least 1 argument must be supplied. Use -h for help.", call.=FALSE) +} + +setwd(opt$runDir) +# setwd("C:/Users/Simon/Desktop/wd/2024-04-01/modalShiftAnalysis/roadPricing-avenidas-principales/output/relative-income-fare0.001") analysisDir <- "analysis/roadpricing" crs <- "EPSG:4485" @@ -83,5 +97,5 @@ trips_base <- merge(trips_base, points_start[, c("trip_id", "start_within")], by filter(!is.na(start_within) | !is.na(end_within)) %>% select(-geometry.x, -geometry.y, -start_within, -end_within) -plotModalShiftSankey(trips_base, trips_roadPricing, dump.output.to = getwd()) -write.csv(trips_roadPricing, file=paste0(getwd(), analysisDir, "/output_trips.roadPricing-area.csv.gz"), quote=FALSE) +plotModalShiftSankey(trips_base, trips_roadPricing, dump.output.to = analysisDir) +write.csv(trips_roadPricing, file=paste0(analysisDir, "/output_trips.roadPricing-area.csv.gz"), quote=FALSE) From aad019f574f8247edb1d98d855abaa94a63fd3ca Mon Sep 17 00:00:00 2001 From: simei94 Date: Thu, 4 Apr 2024 18:58:00 -0600 Subject: [PATCH 11/12] add income distr analysis based on shp file --- .../IncomeDistributionAnalysis.java | 108 ++++++++++++++++++ .../prepare/population/PrepareIncome.java | 4 +- 2 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/matsim/analysis/roadpricing/IncomeDistributionAnalysis.java diff --git a/src/main/java/org/matsim/analysis/roadpricing/IncomeDistributionAnalysis.java b/src/main/java/org/matsim/analysis/roadpricing/IncomeDistributionAnalysis.java new file mode 100644 index 0000000..18aa980 --- /dev/null +++ b/src/main/java/org/matsim/analysis/roadpricing/IncomeDistributionAnalysis.java @@ -0,0 +1,108 @@ +package org.matsim.analysis.roadpricing; + +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.lang.math.DoubleRange; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.operation.buffer.BufferOp; +import org.locationtech.jts.operation.buffer.BufferParameters; +import org.matsim.application.MATSimAppCommand; +import org.matsim.application.options.ShpOptions; +import org.matsim.prepare.population.PrepareIncome; +import org.opengis.feature.simple.SimpleFeature; +import picocli.CommandLine; + +import java.io.FileWriter; +import java.nio.file.Path; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.*; + +public class IncomeDistributionAnalysis implements MATSimAppCommand { + + @CommandLine.Option(names = "--income-shp", description = "Path to shp file with income information", required = true) + private String incomeShpPath; + @CommandLine.Option(names = "--road-pricing-area-shp", description = "Path to shp file of road pricing area", required = true) + private String roadPricingShpPath; + @CommandLine.Option(names = "--output", description = "Path to output folder", required = true) + private Path output; + + private final Map incomeGroupCount = new HashMap<>(); + private final Map incomeGroups = new HashMap<>(); + + public static void main(String[] args) { + new IncomeDistributionAnalysis().execute(args); + } + + @Override + public Integer call() throws Exception { + + ShpOptions incomeShp = new ShpOptions(incomeShpPath, null, null); + Geometry roadPricingArea = new ShpOptions(roadPricingShpPath, null, null).getGeometry(); + + // data from https://www.economia.com.mx/niveles_de_ingreso.htm / amai.org for 2005 + incomeGroupCount.put("E", 0); + incomeGroupCount.put("D_me", 0); + incomeGroupCount.put("D_mas", 0); + incomeGroupCount.put("C_menos", 0); + incomeGroupCount.put("C_me", 0); + incomeGroupCount.put("C_mas", 0); + incomeGroupCount.put("AB", 0); + incomeGroupCount.put("#N/A", 0); + + PrepareIncome.prepareIncomeGroupsMap(incomeGroups); + + double inflationFactor = 1.6173; + +// apply factor to calc 2017 income values for 2005 income values +// for (Map.Entry entry : incomeGroups.entrySet()) { +// incomeGroups.replace(entry.getKey(), new DoubleRange(entry.getValue().getMinimumDouble() * inflationFactor, entry.getValue().getMaximumDouble() * inflationFactor)); +// } + +// avg hh size 3.6 according to ENIGH 2018, see class PrepareIncome + int avgHHSize = 4; + + incomeGroups.forEach((key, value) -> incomeGroups.replace(key, new DoubleRange(value.getMinimumDouble() * inflationFactor / avgHHSize, value.getMaximumDouble() * inflationFactor / 4))); + incomeGroups.put("#N/A", new DoubleRange(0, 999999999)); + + Map geometries = new HashMap<>(); + + for (SimpleFeature feature : incomeShp.readFeatures()) { + Geometry geom; + if (!(((Geometry) feature.getDefaultGeometry()).isValid())) { + geom = BufferOp.bufferOp((Geometry) feature.getDefaultGeometry(), 0.0, BufferParameters.CAP_ROUND); + } else { + geom = (Geometry) feature.getDefaultGeometry(); + } + geometries.put(geom, feature.getAttribute("amai").toString()); + } + + for (Map.Entry e : geometries.entrySet()) { + if (e.getKey().getCentroid().within(roadPricingArea)) { + incomeGroupCount.replace(e.getValue(), incomeGroupCount.get(e.getValue()) + 1); + } + } + +// c_me and c_menos are the same + incomeGroupCount.replace("C_me", incomeGroupCount.get("C_me") + incomeGroupCount.get("C_menos")); + incomeGroupCount.remove("C_menos"); + + int sum = incomeGroupCount.values().stream().mapToInt(Integer::intValue).sum(); + + List sortedKeys = new ArrayList<>(incomeGroupCount.keySet()); + Collections.sort(sortedKeys); + + DecimalFormat f = new DecimalFormat("0.00", new DecimalFormatSymbols(Locale.ENGLISH)); + + try (CSVPrinter printer = new CSVPrinter(new FileWriter(output + "/income-level-distr.csv"), CSVFormat.DEFAULT)) { + printer.printRecord("incomeGroup", "incomeRangePerPerson2017", "count", "share"); + + for (String s : sortedKeys) { + String range = f.format(incomeGroups.get(s).getMinimumDouble()) + "-" + f.format(incomeGroups.get(s).getMaximumDouble()); + printer.printRecord(s, range, incomeGroupCount.get(s), f.format(incomeGroupCount.get(s) / (double) sum)); + } + } + + return 0; + } +} diff --git a/src/main/java/org/matsim/prepare/population/PrepareIncome.java b/src/main/java/org/matsim/prepare/population/PrepareIncome.java index 06e152a..cd5f171 100644 --- a/src/main/java/org/matsim/prepare/population/PrepareIncome.java +++ b/src/main/java/org/matsim/prepare/population/PrepareIncome.java @@ -62,7 +62,7 @@ public Integer call() throws Exception { /** * puts values for income groups into a map. */ - public static void prepareIncomeGroupsMap() { + public static void prepareIncomeGroupsMap(Map incomeGroups) { // data from https://www.economia.com.mx/niveles_de_ingreso.htm / amai.org for 2005 incomeGroups.put("E", new DoubleRange(0., 2699.)); incomeGroups.put("D_me", new DoubleRange(2700., 6799.)); @@ -78,7 +78,7 @@ public static void prepareIncomeGroupsMap() { */ public static void assignIncomeAttr(ShpOptions shp, Population population) { - prepareIncomeGroupsMap(); + prepareIncomeGroupsMap(incomeGroups); // shp file contains the avg familiar income group based on analysis of ITDP México Map geometries = new HashMap<>(); From c18f775997cb2e34be81275eda6147a027cf5d84 Mon Sep 17 00:00:00 2001 From: simei94 Date: Thu, 4 Apr 2024 18:58:00 -0600 Subject: [PATCH 12/12] add income distr analysis based on shp file --- .../analysis/roadpricing/IncomeDistributionAnalysis.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/matsim/analysis/roadpricing/IncomeDistributionAnalysis.java b/src/main/java/org/matsim/analysis/roadpricing/IncomeDistributionAnalysis.java index 18aa980..c676e35 100644 --- a/src/main/java/org/matsim/analysis/roadpricing/IncomeDistributionAnalysis.java +++ b/src/main/java/org/matsim/analysis/roadpricing/IncomeDistributionAnalysis.java @@ -18,6 +18,9 @@ import java.text.DecimalFormatSymbols; import java.util.*; +/** + * analysis for determining the income distribution of agebs for a given road pricing area. + */ public class IncomeDistributionAnalysis implements MATSimAppCommand { @CommandLine.Option(names = "--income-shp", description = "Path to shp file with income information", required = true) @@ -54,14 +57,10 @@ public Integer call() throws Exception { double inflationFactor = 1.6173; -// apply factor to calc 2017 income values for 2005 income values -// for (Map.Entry entry : incomeGroups.entrySet()) { -// incomeGroups.replace(entry.getKey(), new DoubleRange(entry.getValue().getMinimumDouble() * inflationFactor, entry.getValue().getMaximumDouble() * inflationFactor)); -// } - // avg hh size 3.6 according to ENIGH 2018, see class PrepareIncome int avgHHSize = 4; + // apply factor to calc 2017 income values for 2005 income values incomeGroups.forEach((key, value) -> incomeGroups.replace(key, new DoubleRange(value.getMinimumDouble() * inflationFactor / avgHHSize, value.getMaximumDouble() * inflationFactor / 4))); incomeGroups.put("#N/A", new DoubleRange(0, 999999999));