|
| 1 | +package org.matsim.application.analysis.activity; |
| 2 | + |
| 3 | +import it.unimi.dsi.fastutil.objects.Object2IntMap; |
| 4 | +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; |
| 5 | +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; |
| 6 | +import org.apache.logging.log4j.LogManager; |
| 7 | +import org.apache.logging.log4j.Logger; |
| 8 | +import org.geotools.api.feature.Property; |
| 9 | +import org.geotools.api.feature.simple.SimpleFeature; |
| 10 | +import org.locationtech.jts.geom.Geometry; |
| 11 | +import org.matsim.api.core.v01.Coord; |
| 12 | +import org.matsim.application.CommandSpec; |
| 13 | +import org.matsim.application.MATSimAppCommand; |
| 14 | +import org.matsim.application.options.*; |
| 15 | +import org.matsim.core.utils.io.IOUtils; |
| 16 | +import picocli.CommandLine; |
| 17 | +import tech.tablesaw.api.*; |
| 18 | +import tech.tablesaw.io.csv.CsvReadOptions; |
| 19 | +import tech.tablesaw.selection.Selection; |
| 20 | + |
| 21 | +import java.util.*; |
| 22 | +import java.util.regex.Pattern; |
| 23 | + |
| 24 | +@CommandSpec( |
| 25 | + requires = {"activities.csv"}, |
| 26 | + produces = {"activities_%s_per_region.csv"} |
| 27 | +) |
| 28 | +public class ActivityCountAnalysis implements MATSimAppCommand { |
| 29 | + |
| 30 | + private static final Logger log = LogManager.getLogger(ActivityCountAnalysis.class); |
| 31 | + |
| 32 | + @CommandLine.Mixin |
| 33 | + private final InputOptions input = InputOptions.ofCommand(ActivityCountAnalysis.class); |
| 34 | + @CommandLine.Mixin |
| 35 | + private final OutputOptions output = OutputOptions.ofCommand(ActivityCountAnalysis.class); |
| 36 | + @CommandLine.Mixin |
| 37 | + private ShpOptions shp; |
| 38 | + @CommandLine.Mixin |
| 39 | + private SampleOptions sample; |
| 40 | + @CommandLine.Mixin |
| 41 | + private CrsOptions crs; |
| 42 | + |
| 43 | + /** |
| 44 | + * Specifies the column in the shapefile used as the region ID. |
| 45 | + */ |
| 46 | + @CommandLine.Option(names = "--id-column", description = "Column to use as ID for the shapefile", required = true) |
| 47 | + private String idColumn; |
| 48 | + |
| 49 | + /** |
| 50 | + * Maps patterns to merge activity types into a single category. |
| 51 | + * Example: `home;work` can merge activities "home1" and "work1" into categories "home" and "work". |
| 52 | + */ |
| 53 | + @CommandLine.Option(names = "--activity-mapping", description = "Map of patterns to merge activity types", split = ";") |
| 54 | + private Map<String, String> activityMapping; |
| 55 | + |
| 56 | + /** |
| 57 | + * Specifies activity types that should be counted only once per agent per region. |
| 58 | + */ |
| 59 | + @CommandLine.Option(names = "--single-occurrence", description = "Activity types that are only counted once per agent") |
| 60 | + private Set<String> singleOccurrence; |
| 61 | + |
| 62 | + public static void main(String[] args) { |
| 63 | + new ActivityCountAnalysis().execute(args); |
| 64 | + } |
| 65 | + |
| 66 | + /** |
| 67 | + * Executes the activity count analysis. |
| 68 | + * |
| 69 | + * @return Exit code (0 for success). |
| 70 | + * @throws Exception if errors occur during execution. |
| 71 | + */ |
| 72 | + @Override |
| 73 | + public Integer call() throws Exception { |
| 74 | + |
| 75 | + // Prepares the activity mappings and reads input data |
| 76 | + HashMap<String, Set<String>> formattedActivityMapping = new HashMap<>(); |
| 77 | + Map<String, Double> regionAreaMap = new HashMap<>(); |
| 78 | + |
| 79 | + if (this.activityMapping == null) this.activityMapping = new HashMap<>(); |
| 80 | + |
| 81 | + for (Map.Entry<String, String> entry : this.activityMapping.entrySet()) { |
| 82 | + String pattern = entry.getKey(); |
| 83 | + String activity = entry.getValue(); |
| 84 | + Set<String> activities = new HashSet<>(Arrays.asList(activity.split(","))); |
| 85 | + formattedActivityMapping.put(pattern, activities); |
| 86 | + } |
| 87 | + |
| 88 | + // Reading the input csv |
| 89 | + Table activities = Table.read().csv(CsvReadOptions.builder(IOUtils.getBufferedReader(input.getPath("activities.csv"))) |
| 90 | + .columnTypesPartial(Map.of("person", ColumnType.TEXT, "activity_type", ColumnType.TEXT)) |
| 91 | + .sample(false) |
| 92 | + .separator(CsvOptions.detectDelimiter(input.getPath("activities.csv"))).build()); |
| 93 | + |
| 94 | + // remove the underscore and the number from the activity_type column |
| 95 | + TextColumn activityType = activities.textColumn("activity_type"); |
| 96 | + activityType.set(Selection.withRange(0, activityType.size()), activityType.replaceAll("_[0-9]{2,}$", "")); |
| 97 | + |
| 98 | + ShpOptions.Index index = crs.getInputCRS() == null ? shp.createIndex(idColumn) : shp.createIndex(crs.getInputCRS(), idColumn); |
| 99 | + |
| 100 | + // stores the counts of activities per region |
| 101 | + Object2ObjectOpenHashMap<Object, Object2IntMap<String>> regionActivityCounts = new Object2ObjectOpenHashMap<>(); |
| 102 | + // stores the activities that have been counted for each person in each region |
| 103 | + Object2ObjectOpenHashMap<Object, Set<String>> personActivityTracker = new Object2ObjectOpenHashMap<>(); |
| 104 | + |
| 105 | + // iterate over the csv rows |
| 106 | + for (Row row : activities) { |
| 107 | + String person = row.getString("person"); |
| 108 | + String activity = row.getText("activity_type"); |
| 109 | + |
| 110 | + for (Map.Entry<String, Set<String>> entry : formattedActivityMapping.entrySet()) { |
| 111 | + String pattern = entry.getKey(); |
| 112 | + Set<String> activities2 = entry.getValue(); |
| 113 | + for (String act : activities2) { |
| 114 | + if (Pattern.matches(act, activity)) { |
| 115 | + activity = pattern; |
| 116 | + break; |
| 117 | + } |
| 118 | + } |
| 119 | + } |
| 120 | + |
| 121 | + Coord coord = new Coord(row.getDouble("coord_x"), row.getDouble("coord_y")); |
| 122 | + |
| 123 | + // get the region for the current coordinate |
| 124 | + SimpleFeature feature = index.queryFeature(coord); |
| 125 | + |
| 126 | + if (feature == null) { |
| 127 | + continue; |
| 128 | + } |
| 129 | + |
| 130 | + Geometry geometry = (Geometry) feature.getDefaultGeometry(); |
| 131 | + |
| 132 | + Property prop = feature.getProperty(idColumn); |
| 133 | + if (prop == null) |
| 134 | + throw new IllegalArgumentException("No property found for column %s".formatted(idColumn)); |
| 135 | + |
| 136 | + Object region = prop.getValue(); |
| 137 | + if (region != null && region.toString().length() > 0) { |
| 138 | + |
| 139 | + double area = geometry.getArea(); |
| 140 | + regionAreaMap.put(region.toString(), area); |
| 141 | + |
| 142 | + // Add region to the activity counts and person activity tracker if not already present |
| 143 | + regionActivityCounts.computeIfAbsent(region, k -> new Object2IntOpenHashMap<>()); |
| 144 | + personActivityTracker.computeIfAbsent(region, k -> new HashSet<>()); |
| 145 | + |
| 146 | + Set<String> trackedActivities = personActivityTracker.get(region); |
| 147 | + String personActivityKey = person + "_" + activity; |
| 148 | + |
| 149 | + // adding activity only if it has not been counted for the person in the region |
| 150 | + if (singleOccurrence == null || !singleOccurrence.contains(activity) || !trackedActivities.contains(personActivityKey)) { |
| 151 | + Object2IntMap<String> activityCounts = regionActivityCounts.get(region); |
| 152 | + activityCounts.mergeInt(activity, 1, Integer::sum); |
| 153 | + |
| 154 | + // mark the activity as counted for the person in the region |
| 155 | + trackedActivities.add(personActivityKey); |
| 156 | + } |
| 157 | + } |
| 158 | + } |
| 159 | + |
| 160 | + Set<String> uniqueActivities = new HashSet<>(); |
| 161 | + |
| 162 | + for (Object2IntMap<String> map : regionActivityCounts.values()) { |
| 163 | + uniqueActivities.addAll(map.keySet()); |
| 164 | + } |
| 165 | + |
| 166 | + for (String activity : uniqueActivities) { |
| 167 | + Table resultTable = Table.create(); |
| 168 | + TextColumn regionColumn = TextColumn.create("id"); |
| 169 | + DoubleColumn activityColumn = DoubleColumn.create("count"); |
| 170 | + DoubleColumn distributionColumn = DoubleColumn.create("relative_density"); |
| 171 | + DoubleColumn countRatioColumn = DoubleColumn.create("density"); |
| 172 | + DoubleColumn areaColumn = DoubleColumn.create("area"); |
| 173 | + |
| 174 | + resultTable.addColumns(regionColumn, activityColumn, distributionColumn, countRatioColumn, areaColumn); |
| 175 | + for (Map.Entry<Object, Object2IntMap<String>> entry : regionActivityCounts.entrySet()) { |
| 176 | + Object region = entry.getKey(); |
| 177 | + double value = 0; |
| 178 | + for (Map.Entry<String, Integer> entry2 : entry.getValue().object2IntEntrySet()) { |
| 179 | + String ect = entry2.getKey(); |
| 180 | + if (Pattern.matches(ect, activity)) { |
| 181 | + value = entry2.getValue() * sample.getUpscaleFactor(); |
| 182 | + break; |
| 183 | + } |
| 184 | + } |
| 185 | + |
| 186 | + |
| 187 | + Row row = resultTable.appendRow(); |
| 188 | + row.setString("id", region.toString()); |
| 189 | + row.setDouble("count", value); |
| 190 | + } |
| 191 | + |
| 192 | + for (Row row : resultTable) { |
| 193 | + Double area = regionAreaMap.get(row.getString("id")); |
| 194 | + if (area != null) { |
| 195 | + row.setDouble("area", area); |
| 196 | + row.setDouble("density", row.getDouble("count") / area); |
| 197 | + } else { |
| 198 | + log.warn("Area for region {} is not found", row.getString("id")); |
| 199 | + } |
| 200 | + } |
| 201 | + |
| 202 | + Double averageDensity = countRatioColumn.mean(); |
| 203 | + |
| 204 | + for (Row row : resultTable) { |
| 205 | + Double value = row.getDouble("density"); |
| 206 | + if (averageDensity != 0) { |
| 207 | + row.setDouble("relative_density", value / averageDensity); |
| 208 | + } else { |
| 209 | + row.setDouble("relative_density", 0.0); |
| 210 | + } |
| 211 | + } |
| 212 | + |
| 213 | + |
| 214 | + resultTable.write().csv(output.getPath("activities_%s_per_region.csv", activity).toFile()); |
| 215 | + log.info("Wrote activity counts for {} to {}", activity, output.getPath("activities_%s_per_region.csv", activity)); |
| 216 | + } |
| 217 | + |
| 218 | + return 0; |
| 219 | + } |
| 220 | +} |
0 commit comments