traccess 0.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- traccess-0.0.2/LICENSE +21 -0
- traccess-0.0.2/PKG-INFO +24 -0
- traccess-0.0.2/README.md +14 -0
- traccess-0.0.2/pyproject.toml +3 -0
- traccess-0.0.2/setup.cfg +24 -0
- traccess-0.0.2/setup.py +3 -0
- traccess-0.0.2/src/traccess/__init__.py +6 -0
- traccess-0.0.2/src/traccess/access.py +422 -0
- traccess-0.0.2/src/traccess/data.py +210 -0
- traccess-0.0.2/src/traccess/exception.py +1 -0
- traccess-0.0.2/src/traccess.egg-info/PKG-INFO +24 -0
- traccess-0.0.2/src/traccess.egg-info/SOURCES.txt +17 -0
- traccess-0.0.2/src/traccess.egg-info/dependency_links.txt +1 -0
- traccess-0.0.2/src/traccess.egg-info/requires.txt +1 -0
- traccess-0.0.2/src/traccess.egg-info/top_level.txt +1 -0
- traccess-0.0.2/tests/test_access.py +11 -0
- traccess-0.0.2/tests/test_cost.py +7 -0
- traccess-0.0.2/tests/test_supply.py +8 -0
traccess-0.0.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Willem Klumpenhouwer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
traccess-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: traccess
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Transportation access to opportunities and equity analysis
|
|
5
|
+
Author: Willem Klumpenhouwer
|
|
6
|
+
Author-email: willem@klumpentown.com
|
|
7
|
+
License: MIT
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
|
|
11
|
+
# Traccess
|
|
12
|
+
Transportation access and equity computations.
|
|
13
|
+
|
|
14
|
+
Traccess offers a set of fast and convenient functions to calculate multiple
|
|
15
|
+
transport accessibility measures. Given a pre-computed travel cost matrix, and
|
|
16
|
+
using data sets on land use supply, demand, and demographics, the package
|
|
17
|
+
computes accessibility levels using multiple accessibility measures, such as:
|
|
18
|
+
cumulative opportunities, minimum travel cost to closest *n* number of activities,
|
|
19
|
+
gravity-based (with different decay functions) and different floating catchment
|
|
20
|
+
area methods.
|
|
21
|
+
|
|
22
|
+
The package also contains a number of different methods for computing
|
|
23
|
+
distributive and sufficientarian equity measures to compare the level of access
|
|
24
|
+
provided across demographic groups.
|
traccess-0.0.2/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Traccess
|
|
2
|
+
Transportation access and equity computations.
|
|
3
|
+
|
|
4
|
+
Traccess offers a set of fast and convenient functions to calculate multiple
|
|
5
|
+
transport accessibility measures. Given a pre-computed travel cost matrix, and
|
|
6
|
+
using data sets on land use supply, demand, and demographics, the package
|
|
7
|
+
computes accessibility levels using multiple accessibility measures, such as:
|
|
8
|
+
cumulative opportunities, minimum travel cost to closest *n* number of activities,
|
|
9
|
+
gravity-based (with different decay functions) and different floating catchment
|
|
10
|
+
area methods.
|
|
11
|
+
|
|
12
|
+
The package also contains a number of different methods for computing
|
|
13
|
+
distributive and sufficientarian equity measures to compare the level of access
|
|
14
|
+
provided across demographic groups.
|
traccess-0.0.2/setup.cfg
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[metadata]
|
|
2
|
+
name = traccess
|
|
3
|
+
version = attr: traccess.__version__
|
|
4
|
+
author = Willem Klumpenhouwer
|
|
5
|
+
author_email = willem@klumpentown.com
|
|
6
|
+
description = Transportation access to opportunities and equity analysis
|
|
7
|
+
long_description = file: README.md
|
|
8
|
+
long_description_content_type = text/markdown
|
|
9
|
+
license = MIT
|
|
10
|
+
|
|
11
|
+
[options]
|
|
12
|
+
packages = find:
|
|
13
|
+
install_requires =
|
|
14
|
+
pandas >= 2.0
|
|
15
|
+
package_dir =
|
|
16
|
+
=src
|
|
17
|
+
|
|
18
|
+
[options.packages.find]
|
|
19
|
+
where = src
|
|
20
|
+
|
|
21
|
+
[egg_info]
|
|
22
|
+
tag_build =
|
|
23
|
+
tag_date = 0
|
|
24
|
+
|
traccess-0.0.2/setup.py
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import numpy
|
|
2
|
+
import pandas
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
from .data import Cost, Demand, Demographic, Supply, Access
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AccessComputer:
|
|
9
|
+
"""Compute access to destinations."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
supply: Supply,
|
|
14
|
+
cost: Cost,
|
|
15
|
+
demand: Demand = None,
|
|
16
|
+
):
|
|
17
|
+
"""Compute access to opportunity metrics.
|
|
18
|
+
|
|
19
|
+
The access computer uses supplied datasets on supply, cost, and
|
|
20
|
+
optionally demand to compute popular access to opportunity measures.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
supply : Supply
|
|
25
|
+
The supply or destination set to compute access to
|
|
26
|
+
cost : Cost
|
|
27
|
+
A matrix of cost values (travel time, other cost, etc)
|
|
28
|
+
demand : Demand, optional
|
|
29
|
+
The demand for destinations, used for competitive measures
|
|
30
|
+
"""
|
|
31
|
+
if not isinstance(supply, Supply):
|
|
32
|
+
raise TypeError(
|
|
33
|
+
"The supply must be of type Supply. Check the order of arguments passed."
|
|
34
|
+
)
|
|
35
|
+
if not isinstance(cost, Cost):
|
|
36
|
+
raise TypeError(
|
|
37
|
+
"The cost must be of type Cost. Check the order of arguments passed."
|
|
38
|
+
)
|
|
39
|
+
self._supply = supply
|
|
40
|
+
self._cost = cost
|
|
41
|
+
self._demand = demand
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def cost(self) -> Cost:
|
|
45
|
+
"""Access or set the cost object"""
|
|
46
|
+
return self._cost
|
|
47
|
+
|
|
48
|
+
@cost.setter
|
|
49
|
+
def cost(self, cost: Cost):
|
|
50
|
+
self._cost = cost
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def supply(self):
|
|
54
|
+
"""Access or set the supply object"""
|
|
55
|
+
return self._supply
|
|
56
|
+
|
|
57
|
+
@supply.setter
|
|
58
|
+
def supply(self, supply: Supply):
|
|
59
|
+
self._supply = supply
|
|
60
|
+
|
|
61
|
+
def cost_to_closest(
|
|
62
|
+
self, cost_column: str, supply_columns: list[str], n=1
|
|
63
|
+
) -> Access:
|
|
64
|
+
"""Compute the cost to the nth closest destination.
|
|
65
|
+
|
|
66
|
+
This function is generic over any kind of numeric travel cost, such as
|
|
67
|
+
distance, time and money.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
cost_column : str
|
|
72
|
+
The column in the Cost object that contains the travel cost
|
|
73
|
+
supply_columns : list[str]
|
|
74
|
+
The columns in the Supply data to compute access to
|
|
75
|
+
n : int, optional
|
|
76
|
+
The nth closest to compute, by default 1
|
|
77
|
+
|
|
78
|
+
Returns
|
|
79
|
+
-------
|
|
80
|
+
Access
|
|
81
|
+
An Access data object with an `id_column` matching the origin zone.
|
|
82
|
+
"""
|
|
83
|
+
# First we join the destinations
|
|
84
|
+
with_dest = self.cost.data.reset_index().set_index(self.cost._to_id)
|
|
85
|
+
with_dest = with_dest.join(self.supply.data, how="right")
|
|
86
|
+
|
|
87
|
+
if isinstance(supply_columns, str):
|
|
88
|
+
supply_columns = [supply_columns]
|
|
89
|
+
|
|
90
|
+
columns = []
|
|
91
|
+
|
|
92
|
+
for c in supply_columns:
|
|
93
|
+
# Keep only columns with actual destinations
|
|
94
|
+
this_column = with_dest[with_dest[c] > 0]
|
|
95
|
+
|
|
96
|
+
result = (
|
|
97
|
+
this_column[[self.cost._from_id, cost_column, c]]
|
|
98
|
+
.groupby(self.cost._from_id)
|
|
99
|
+
.apply(_get_nth, o=c, n=n, cost=cost_column)
|
|
100
|
+
)
|
|
101
|
+
columns.append(result)
|
|
102
|
+
|
|
103
|
+
final = pandas.concat(columns, axis="columns", join="outer")
|
|
104
|
+
|
|
105
|
+
final.columns = supply_columns
|
|
106
|
+
|
|
107
|
+
final = final.reset_index()
|
|
108
|
+
|
|
109
|
+
return Access(final, id_column=self.cost._from_id)
|
|
110
|
+
|
|
111
|
+
def cumulative_cutoff(
|
|
112
|
+
self,
|
|
113
|
+
cost_columns: list[str],
|
|
114
|
+
cutoffs: list[float],
|
|
115
|
+
supply_columns: list[str] = None,
|
|
116
|
+
) -> Access:
|
|
117
|
+
"""Compute the total number of opportunities within a specified travel
|
|
118
|
+
cost cutoff.
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
cost_columns : list[str]
|
|
123
|
+
A list of cost columns to apply cutoffs to. Can be a single string
|
|
124
|
+
for a single column.
|
|
125
|
+
cutoffs : list[float]
|
|
126
|
+
A list of cutoff values corresponding to each cost column. Can be a
|
|
127
|
+
single value for a single cutoff column. Must be the same length as
|
|
128
|
+
`cost_columns`.
|
|
129
|
+
supply_columns : list[str]
|
|
130
|
+
An optional list of supply columns to use and return. If None, all
|
|
131
|
+
supply columns are used. By default, None.
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
Access
|
|
136
|
+
An Access data object with an `id_column` matching the origin zone.
|
|
137
|
+
|
|
138
|
+
Raises
|
|
139
|
+
------
|
|
140
|
+
ValueError
|
|
141
|
+
If the supplied column list and cutoff lists are not the same length.
|
|
142
|
+
"""
|
|
143
|
+
if isinstance(cost_columns, str):
|
|
144
|
+
cost_columns = [cost_columns]
|
|
145
|
+
if isinstance(cutoffs, float) or isinstance(cutoffs, int):
|
|
146
|
+
cutoffs = [cutoffs]
|
|
147
|
+
if isinstance(supply_columns, str):
|
|
148
|
+
supply_columns = [supply_columns]
|
|
149
|
+
|
|
150
|
+
if len(cost_columns) != len(cutoffs):
|
|
151
|
+
raise ValueError("Cost and cutoff columns must be the same length")
|
|
152
|
+
|
|
153
|
+
join_column = self.cost._to_id
|
|
154
|
+
group_column = self.cost._from_id
|
|
155
|
+
|
|
156
|
+
# Set the join index and join
|
|
157
|
+
df = self.cost.data.reset_index().set_index(join_column)
|
|
158
|
+
|
|
159
|
+
if supply_columns is None:
|
|
160
|
+
supply_columns = self.supply.columns
|
|
161
|
+
|
|
162
|
+
df = df.join(self.supply.data[supply_columns])
|
|
163
|
+
|
|
164
|
+
df["_weights"] = 1.0
|
|
165
|
+
# Iterate through the columns and update weights based on columns
|
|
166
|
+
for idx, c in enumerate(cost_columns):
|
|
167
|
+
df["_weights"] = numpy.where(df[c] <= cutoffs[idx], df["_weights"], 0)
|
|
168
|
+
|
|
169
|
+
# Multily all opportunities by the weights
|
|
170
|
+
df[supply_columns] = df[supply_columns].multiply(df["_weights"], axis="index")
|
|
171
|
+
# Set the group columns
|
|
172
|
+
df.index.rename(join_column, inplace=True)
|
|
173
|
+
df.reset_index(inplace=True)
|
|
174
|
+
# Group and return
|
|
175
|
+
columns = [group_column]
|
|
176
|
+
columns.extend(supply_columns)
|
|
177
|
+
access_df = df[columns].groupby(group_column).sum().reset_index()
|
|
178
|
+
return Access(
|
|
179
|
+
access_df,
|
|
180
|
+
id_column=group_column,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def cumulative_decay(self, cost_columns: list[str], decay_function) -> Access:
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class EquityComputer:
|
|
188
|
+
"""Compute distributive equity across population demograhics"""
|
|
189
|
+
|
|
190
|
+
def __init__(self, access: Access, demographic: Demographic):
|
|
191
|
+
"""Compute distributive equity metrics across population demographics
|
|
192
|
+
|
|
193
|
+
An EquityComputer allows for the combination of access measures and
|
|
194
|
+
demographic data to compute various metrics of distributive equity or
|
|
195
|
+
transportation justice.
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
access : Access
|
|
200
|
+
An `Access` object which contains data on access to opportunities.
|
|
201
|
+
demographic : Demographic
|
|
202
|
+
A `Demographic` data object which contains data on demographics and
|
|
203
|
+
populations
|
|
204
|
+
"""
|
|
205
|
+
self._access = access
|
|
206
|
+
self._demographic = demographic
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def access(self) -> Access:
|
|
210
|
+
return self._access
|
|
211
|
+
|
|
212
|
+
@access.setter
|
|
213
|
+
def access(self, access: Access):
|
|
214
|
+
assert isinstance(access, Access)
|
|
215
|
+
self._access = access
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def demographic(self) -> Demographic:
|
|
219
|
+
return self._demographic
|
|
220
|
+
|
|
221
|
+
@demographic.setter
|
|
222
|
+
def demographic(self, demographic: Demographic):
|
|
223
|
+
assert isinstance(demographic, Demographic)
|
|
224
|
+
self._demographic = demographic
|
|
225
|
+
|
|
226
|
+
def in_poverty(
|
|
227
|
+
self, access_column: str, poverty_line: float, is_dual=False
|
|
228
|
+
) -> pandas.Series:
|
|
229
|
+
"""Compute the number of individuals in poverty in any given demographic.
|
|
230
|
+
|
|
231
|
+
Parameters
|
|
232
|
+
----------
|
|
233
|
+
access_column : str
|
|
234
|
+
The access column to compare the poverty line to
|
|
235
|
+
poverty_line : float
|
|
236
|
+
The poverty line
|
|
237
|
+
is_dual: bool, optional
|
|
238
|
+
Whether the measure is a dual measure, where lower values are
|
|
239
|
+
better, by default False
|
|
240
|
+
|
|
241
|
+
Returns
|
|
242
|
+
-------
|
|
243
|
+
pandas.Series
|
|
244
|
+
A series with each row consisting of a demogrphic group, containing
|
|
245
|
+
the number of that group in poverty.
|
|
246
|
+
"""
|
|
247
|
+
# Count the number of people in poverty
|
|
248
|
+
df = self.access.data.join(self.demographic.data)
|
|
249
|
+
if is_dual == True:
|
|
250
|
+
df[df[access_column] > poverty_line][self.demographic.columns].sum()
|
|
251
|
+
else:
|
|
252
|
+
return df[df[access_column] < poverty_line][self.demographic.columns].sum()
|
|
253
|
+
|
|
254
|
+
def fgt_poverty(
|
|
255
|
+
self, access_column: str, poverty_line: float, alpha: float, is_dual=False
|
|
256
|
+
) -> pandas.Series:
|
|
257
|
+
"""Compute a Foster-Greer-Thorbecke (FGT) index for all demographics.
|
|
258
|
+
|
|
259
|
+
FGT measures consider the average amount of poverty in a given
|
|
260
|
+
population by comparing the distance of each individual from the poverty
|
|
261
|
+
line, raised to some exponent alpha. Common values of alpha are 0, 1,
|
|
262
|
+
and 2.
|
|
263
|
+
|
|
264
|
+
When alpha = 0, the function returns the poverty rate
|
|
265
|
+
|
|
266
|
+
When alpha = 1, the function returns the poverty gap index
|
|
267
|
+
|
|
268
|
+
When alpha = 2, the function returns the poverty gap weighted by the
|
|
269
|
+
poverty gap.
|
|
270
|
+
|
|
271
|
+
Parameters
|
|
272
|
+
----------
|
|
273
|
+
access_column : str
|
|
274
|
+
The access to opportunity column to compare the measure against
|
|
275
|
+
poverty_line : float
|
|
276
|
+
The poverty line to compare the access measure against
|
|
277
|
+
alpha : float
|
|
278
|
+
The alpha parameter, or the extent which to weight those further
|
|
279
|
+
below the poverty line
|
|
280
|
+
is_dual: bool, optional
|
|
281
|
+
Whether the measure is a dual measure, where lower values are
|
|
282
|
+
better, by default False
|
|
283
|
+
|
|
284
|
+
Returns
|
|
285
|
+
-------
|
|
286
|
+
pandas.Series
|
|
287
|
+
A series with an index for each demographic column, containing the
|
|
288
|
+
specified measure.
|
|
289
|
+
"""
|
|
290
|
+
df = self.access.data.join(self.demographic.data)
|
|
291
|
+
|
|
292
|
+
# Keep the total of each population group
|
|
293
|
+
n = self.demographic.data.sum().rename("n")
|
|
294
|
+
|
|
295
|
+
if is_dual == True:
|
|
296
|
+
df["_delta"] = df[access_column] - poverty_line
|
|
297
|
+
else:
|
|
298
|
+
df["_delta"] = poverty_line - df[access_column]
|
|
299
|
+
|
|
300
|
+
# We only do the math on the set of those in poverty
|
|
301
|
+
df = df[df["_delta"] > 0]
|
|
302
|
+
df["_delta"] = df["_delta"] / poverty_line
|
|
303
|
+
df["_delta"] = df["_delta"].pow(alpha)
|
|
304
|
+
df[self.demographic.columns] = df[self.demographic.columns].multiply(
|
|
305
|
+
df["_delta"], axis="index"
|
|
306
|
+
)
|
|
307
|
+
totals = df[self.demographic.columns].sum().rename("count")
|
|
308
|
+
totals = pandas.concat([totals, n], axis="columns")
|
|
309
|
+
totals["fgt"] = totals["count"] / totals["n"]
|
|
310
|
+
|
|
311
|
+
return totals["fgt"].rename(f"fgt{alpha}")
|
|
312
|
+
|
|
313
|
+
def poverty_index(
|
|
314
|
+
self, access_column: str, poverty_line: float, is_dual=False
|
|
315
|
+
) -> pandas.Series:
|
|
316
|
+
"""Compute the poverty index at each location.
|
|
317
|
+
|
|
318
|
+
This method computes the poverty index for each location, which is the
|
|
319
|
+
difference between the poverty line and the supplied access value,
|
|
320
|
+
divided by the poverty line.
|
|
321
|
+
|
|
322
|
+
Values above the poverty line are returned as null values.
|
|
323
|
+
|
|
324
|
+
Parameters
|
|
325
|
+
----------
|
|
326
|
+
access_column : str
|
|
327
|
+
The column to compare the poverty line to
|
|
328
|
+
poverty_line : float
|
|
329
|
+
The poverty line value for access
|
|
330
|
+
is_dual: bool, optional
|
|
331
|
+
Whether the measure is a dual measure, where lower values are
|
|
332
|
+
better, by default False
|
|
333
|
+
|
|
334
|
+
Returns
|
|
335
|
+
-------
|
|
336
|
+
pandas.Series
|
|
337
|
+
A pandas Series containing the poverty index for each location
|
|
338
|
+
"""
|
|
339
|
+
df = self.access.data.copy()
|
|
340
|
+
if is_dual == True:
|
|
341
|
+
df["poverty_index"] = (df[access_column] - poverty_line) / poverty_line
|
|
342
|
+
else:
|
|
343
|
+
df["poverty_index"] = (poverty_line - df[access_column]) / poverty_line
|
|
344
|
+
df["poverty_index"] = numpy.where(
|
|
345
|
+
df.poverty_index < 0, pandas.NA, df.poverty_index
|
|
346
|
+
)
|
|
347
|
+
return df["poverty_index"]
|
|
348
|
+
|
|
349
|
+
def weighted_average(self, access_column: str) -> pandas.Series:
|
|
350
|
+
"""Compute the population group-weighted average access for all groups.
|
|
351
|
+
|
|
352
|
+
Parameters
|
|
353
|
+
----------
|
|
354
|
+
access_column : str
|
|
355
|
+
The access value to weight
|
|
356
|
+
|
|
357
|
+
Returns
|
|
358
|
+
-------
|
|
359
|
+
pandas.Series
|
|
360
|
+
A series with a row for demographic group, with the weighted average
|
|
361
|
+
access.
|
|
362
|
+
"""
|
|
363
|
+
df = self.access.data.join(self.demographic.data)
|
|
364
|
+
# Normalize the population columns
|
|
365
|
+
for c in self.demographic.columns:
|
|
366
|
+
df[c] = df[c] / df[c].sum()
|
|
367
|
+
# Multiply and sum
|
|
368
|
+
df[self.demographic.columns] = df[self.demographic.columns].multiply(
|
|
369
|
+
df[access_column], axis="index"
|
|
370
|
+
)
|
|
371
|
+
df = df[self.demographic.columns].sum()
|
|
372
|
+
df = df.rename(access_column)
|
|
373
|
+
return df
|
|
374
|
+
|
|
375
|
+
def weighted_quantile(
|
|
376
|
+
self, access_column: str, quantile=0.5, is_dual=False
|
|
377
|
+
) -> pandas.Series:
|
|
378
|
+
"""Compute a population-weighted quantile for all demographics.
|
|
379
|
+
|
|
380
|
+
Population-weighted quantiles are *not interpolated*, meaning that the
|
|
381
|
+
values returned by this function are the highest value in the dataset
|
|
382
|
+
not exceeding the quantile value.
|
|
383
|
+
|
|
384
|
+
Parameters
|
|
385
|
+
----------
|
|
386
|
+
access_column : str
|
|
387
|
+
The column to compute the quantile over
|
|
388
|
+
quantile : float, optional
|
|
389
|
+
The quantile to use, by default 0.5
|
|
390
|
+
is_dual: bool, optional
|
|
391
|
+
Whether the measure is a dual measure, where lower values are
|
|
392
|
+
better, by default False
|
|
393
|
+
|
|
394
|
+
Returns
|
|
395
|
+
-------
|
|
396
|
+
pandas.Series
|
|
397
|
+
A series containing each demographic group and the access value of
|
|
398
|
+
that quantile.
|
|
399
|
+
"""
|
|
400
|
+
|
|
401
|
+
if is_dual == True:
|
|
402
|
+
quantile = 1.0 - quantile
|
|
403
|
+
|
|
404
|
+
df = self.access.data.join(self.demographic.data)
|
|
405
|
+
df.sort_values(access_column, inplace=True)
|
|
406
|
+
|
|
407
|
+
result = dict()
|
|
408
|
+
for c in self.demographic.columns:
|
|
409
|
+
total_weight = df[c].sum()
|
|
410
|
+
df["_cumulative"] = df[c].cumsum()
|
|
411
|
+
result[c] = df[df["_cumulative"] <= (total_weight) * quantile][
|
|
412
|
+
access_column
|
|
413
|
+
].iloc[-1]
|
|
414
|
+
|
|
415
|
+
return pandas.Series(result, name=f"q{int(quantile * 100)}")
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _get_nth(df, o, n, cost):
|
|
419
|
+
df = df.sort_values(by=cost, ascending=True, na_position="last")
|
|
420
|
+
df["_cumsum"] = df[o].cumsum()
|
|
421
|
+
df = df[df["_cumsum"] >= n]
|
|
422
|
+
return df[cost].min()
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
import pandas
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AbstractDataSet(ABC):
|
|
7
|
+
"""An abstract data set
|
|
8
|
+
|
|
9
|
+
Parameters
|
|
10
|
+
----------
|
|
11
|
+
dataframe : pandas.DataFrame
|
|
12
|
+
The dataframe containing the data
|
|
13
|
+
id_column : str, optional
|
|
14
|
+
The column which should be used as a unique ID, by default "id"
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, dataframe: pandas.DataFrame, id_column="id"):
|
|
18
|
+
self._data = dataframe.copy()
|
|
19
|
+
self._data.set_index(id_column, inplace=True)
|
|
20
|
+
|
|
21
|
+
self._id = id_column
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def columns(self):
|
|
25
|
+
return [i for i in self._data.columns]
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def data(self) -> pandas.DataFrame:
|
|
29
|
+
return self._data
|
|
30
|
+
|
|
31
|
+
@data.setter
|
|
32
|
+
def data(self, data: pandas.DataFrame):
|
|
33
|
+
self._data = data
|
|
34
|
+
|
|
35
|
+
def normalize(self, columns=None, min=0.0, max=1.0):
|
|
36
|
+
"""Normalize one or more columns between a range
|
|
37
|
+
|
|
38
|
+
This method scales a column based on its minimum
|
|
39
|
+
and maximum values.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
columns : list, optional
|
|
44
|
+
The list of columns to normalise. If None, all columns are used, by default None
|
|
45
|
+
min : float, optional
|
|
46
|
+
The minimum of the scaled range, by default 0
|
|
47
|
+
max : float, optional
|
|
48
|
+
The maximum of the scaled range, by default 1
|
|
49
|
+
"""
|
|
50
|
+
if columns == None:
|
|
51
|
+
columns = self._data.columns
|
|
52
|
+
|
|
53
|
+
for c in columns:
|
|
54
|
+
self._data[c] = (self._data[c] - self._data[c].min()) / (
|
|
55
|
+
self._data[c].max() - self._data[c].min()
|
|
56
|
+
) * (max - min) + min
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_csv(cls, csv_filepath, id_column="id", **kwargs):
|
|
60
|
+
"""Create a Supply object from a csv file
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
csv_filepath : str or path
|
|
65
|
+
The filepath of the CSV to parse
|
|
66
|
+
id_column : str, optional
|
|
67
|
+
The column used as the reference id, by default "id"
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
Supply
|
|
72
|
+
A supply object containing opportunities or land use data
|
|
73
|
+
"""
|
|
74
|
+
dataframe = pandas.read_csv(csv_filepath, **kwargs)
|
|
75
|
+
return cls(dataframe, id_column)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class AbstractMatrix(ABC):
|
|
79
|
+
"""A representation of a matrix object.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
dataframe : pandas.DataFrame
|
|
84
|
+
The dataframe for the matrix object
|
|
85
|
+
from_id : str, optional
|
|
86
|
+
The first reference id (the origin), by default "from_id"
|
|
87
|
+
to_id : str, optional
|
|
88
|
+
The second reference id (the destination), by default "to_id"
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self, dataframe: pandas.DataFrame, from_id="from_id", to_id="to_id"
|
|
93
|
+
) -> None:
|
|
94
|
+
self._data = dataframe.copy()
|
|
95
|
+
self._data.set_index([from_id, to_id], inplace=True)
|
|
96
|
+
|
|
97
|
+
self._from_id = from_id
|
|
98
|
+
self._to_id = to_id
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def columns(self) -> list:
|
|
102
|
+
"""The list of columns in the dataset"""
|
|
103
|
+
return self._data.columns
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def data(self) -> pandas.DataFrame:
|
|
107
|
+
"""The object's dataframe"""
|
|
108
|
+
return self._data
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def from_csv(cls, csv_filepath, from_id="from_id", to_id="to_id", **kwargs):
|
|
112
|
+
"""Load data and create an object from a CSV file
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
csv_filepath : str
|
|
117
|
+
The filepath of the CSV file to load
|
|
118
|
+
from_id : str, optional
|
|
119
|
+
The column name of the origin, by default "from_id"
|
|
120
|
+
to_id : str, optional
|
|
121
|
+
The column name of the destination, by default "to_id"
|
|
122
|
+
|
|
123
|
+
Returns
|
|
124
|
+
-------
|
|
125
|
+
AbstractMatrix
|
|
126
|
+
A matrix dataset.
|
|
127
|
+
"""
|
|
128
|
+
dataframe = pandas.read_csv(csv_filepath, **kwargs)
|
|
129
|
+
return cls(dataframe, from_id, to_id)
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def from_parquet(cls, parquet_filepath, from_id="from_id", to_id="to_id", **kwargs):
|
|
133
|
+
"""Load data and create an object from a Parquet file
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
csv_filepath : str
|
|
138
|
+
The filepath of the Parquet file to load
|
|
139
|
+
from_id : str, optional
|
|
140
|
+
The column name of the origin, by default "from_id"
|
|
141
|
+
to_id : str, optional
|
|
142
|
+
The column name of the destination, by default "to_id"
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
AbstractMatrix
|
|
147
|
+
A matrix dataset.
|
|
148
|
+
"""
|
|
149
|
+
dataframe = pandas.read_parquet(parquet_filepath, **kwargs)
|
|
150
|
+
return cls(dataframe, from_id, to_id)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class Access(AbstractDataSet):
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class Cost(AbstractMatrix):
|
|
158
|
+
def quantile(self, quantile: float, use_to_id=False) -> pandas.DataFrame:
|
|
159
|
+
"""Compute the quantile cost values across all origins or destinations
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
quantile : float
|
|
164
|
+
The quantile to compute (0 to 1)
|
|
165
|
+
use_to_id : bool, optional
|
|
166
|
+
If true, group by the destination column instead of the origin, by default False
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
pandas.DataFrame
|
|
171
|
+
A dataframe with an index for each id and the quantile value
|
|
172
|
+
"""
|
|
173
|
+
if use_to_id:
|
|
174
|
+
return self.data.groupby(self._to_id).quantile(quantile)
|
|
175
|
+
else:
|
|
176
|
+
return self.data.groupby(self._from_id).quantile(quantile)
|
|
177
|
+
|
|
178
|
+
def median(self, use_to_id=False) -> pandas.DataFrame:
|
|
179
|
+
"""Compute the median cost values across all origins or destinations.
|
|
180
|
+
|
|
181
|
+
This function is a shorthand for `Cost.quantile(quantile=0.5)`
|
|
182
|
+
|
|
183
|
+
Parameters
|
|
184
|
+
----------
|
|
185
|
+
use_to_id : bool, optional
|
|
186
|
+
If true, group by the destination column instead of the origin, by default False
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
pandas.DataFrame
|
|
191
|
+
A dataframe with an index for each id and a median value
|
|
192
|
+
"""
|
|
193
|
+
return self.quantile(0.5, use_to_id)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class Demand(AbstractDataSet):
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class Supply(AbstractDataSet):
|
|
201
|
+
def generalized_cost(self):
|
|
202
|
+
raise NotImplementedError
|
|
203
|
+
|
|
204
|
+
def intrazonal(self):
|
|
205
|
+
raise NotImplementedError
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class Demographic(AbstractDataSet):
|
|
209
|
+
def something(self):
|
|
210
|
+
raise NotImplementedError
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: traccess
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Transportation access to opportunities and equity analysis
|
|
5
|
+
Author: Willem Klumpenhouwer
|
|
6
|
+
Author-email: willem@klumpentown.com
|
|
7
|
+
License: MIT
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
|
|
11
|
+
# Traccess
|
|
12
|
+
Transportation access and equity computations.
|
|
13
|
+
|
|
14
|
+
Traccess offers a set of fast and convenient functions to calculate multiple
|
|
15
|
+
transport accessibility measures. Given a pre-computed travel cost matrix, and
|
|
16
|
+
using data sets on land use supply, demand, and demographics, the package
|
|
17
|
+
computes accessibility levels using multiple accessibility measures, such as:
|
|
18
|
+
cumulative opportunities, minimum travel cost to closest *n* number of activities,
|
|
19
|
+
gravity-based (with different decay functions) and different floating catchment
|
|
20
|
+
area methods.
|
|
21
|
+
|
|
22
|
+
The package also contains a number of different methods for computing
|
|
23
|
+
distributive and sufficientarian equity measures to compare the level of access
|
|
24
|
+
provided across demographic groups.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.cfg
|
|
5
|
+
setup.py
|
|
6
|
+
src/traccess/__init__.py
|
|
7
|
+
src/traccess/access.py
|
|
8
|
+
src/traccess/data.py
|
|
9
|
+
src/traccess/exception.py
|
|
10
|
+
src/traccess.egg-info/PKG-INFO
|
|
11
|
+
src/traccess.egg-info/SOURCES.txt
|
|
12
|
+
src/traccess.egg-info/dependency_links.txt
|
|
13
|
+
src/traccess.egg-info/requires.txt
|
|
14
|
+
src/traccess.egg-info/top_level.txt
|
|
15
|
+
tests/test_access.py
|
|
16
|
+
tests/test_cost.py
|
|
17
|
+
tests/test_supply.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pandas>=2.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
traccess
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import traccess
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TestAccessComputer:
|
|
5
|
+
def test_cumulative_cutoff(self, access_object: traccess.AccessComputer):
|
|
6
|
+
df = access_object.cumulative_cutoff(access_object.cost.columns, [29, 2]).data
|
|
7
|
+
assert df.loc[1]["oj"] == 16.0
|
|
8
|
+
|
|
9
|
+
def test_cost_to_closest(self, access_object: traccess.AccessComputer):
|
|
10
|
+
df = access_object.cost_to_closest("c", ["oj2"], n=2).data
|
|
11
|
+
assert df.loc[3]["oj2"] == 20.0
|