Jackson Voelkel
… or you can just watch a video.
Esri is the main GIS software company around today.
Here’s William Shatner talking about one of the first GIS tools!
Geographic space is often the lowest common denomenator for compiling and analyzing disparate data.
First steps in analysis: exploratory and descriptive
Hypothesis testing
Spatial data analysis is concerned with that branch of data analysis where the geographical referencing of objects contains important information. In many areas of data collection, especially in some areas of experimental science, the indexes that distinguish different cases can be exchanged without any loss of information. All the information relevant to understanding the variation in the data set is contained in the observations and no relevant information is contained in the indexing. In the case of spatial data the indexing (by location and time) may contain crucial information. A definition of spatial analysis (of which spatial data analysis is one element) is that it represents a collection of techniques and models that explicitly use the spatial referencing of each data case.
Goodchild, Michael, and Haining. 2004. “GIS and spatial data analysis: converging perspectives”. Papers in Regional Science 83:363-385
Spatial data analysis is concerned with that branch of data analysis where the geographical referencing of objects contains important information. In many areas of data collection, especially in some areas of experimental science, the indexes that distinguish different cases can be exchanged without any loss of information. All the information relevant to understanding the variation in the data set is contained in the observations and no relevant information is contained in the indexing. In the case of spatial data the indexing (by location and time) may contain crucial information. A definition of spatial analysis (of which spatial data analysis is one element) is that it represents a collection of techniques and models that explicitly use the spatial referencing of each data case.
Goodchild, Michael, and Haining. 2004. “GIS and spatial data analysis: converging perspectives”. Papers in Regional Science 83:363-385
Different perspective on spatial data
GIS is built around the entity-attribute model. Spatial analysis uses these data, and sees data as patterns that are the outcomes of processes. This is similar… but different than spatial data manipulation.
We wish to filter a specific cohort of people by distance to rivers, perform and analysis, and push the results back to our DB.
We wish to join the newest Census Blocks (~12 million) to every point we have in a database (lets imagine ~12 million patients, trees, or aardvarks).
Yes, I could do some of this in Oracle Spatial, but I’m not made of money … anyways, I think PostGIS is better!
Well, obviously!
PostGIS!
As easy as…
$ sudo apt install postgresql-10-postgis-2.4
Or the equivalent for your inferior (just kidding) system/version
and
CREATE EXTENSION POSTGIS;
Spatial data works in a standard entity-attribute format, with various geometry types.
The Open Geographic Consortium sets standards, which PostGIS follows.
Let’s look at a very simple data set: fire stations in the Portland Metro Area:
SELECT * FROM fire_sta;
id | geom | station | address | city | district
-----+----------------------------------------------------------------------+---------+-------------------------------------+-----------------+--------------------------------------
1 | 01040000201E6900000100000001010000006848EEED96711F410D8911649E305341 | 50 | 12617 SW WALNUT ST | TIGARD | TUALATIN VALLEY FIRE & RESCUE
2 | 01040000201E6900000100000001010000003057F1DB66D61F41779684954A2D5341 | 211 | 4555 JEAN RD | LAKE OSWEGO | LAKE OSWEGO F&R&LIFE SAFETY
3 | 01040000201E6900000100000001010000000B36E71F0FDB1D412D5B860991315341 | 11 | 102 E MAIN ST | GASTON | GASTON RFPD
4 | 01040000201E6900000100000001010000001E8D7697A0D61F41D67BD6BA5B415341 | 26 | 5247 N LOMBARD ST | PORTLAND | PORTLAND FIRE & RESCUE
5 | 01040000201E6900000100000001010000006F3DEF04B1A51F4197EB934FA6265341 | 56 | 8455 SW ELLIGSEN RD | WILSONVILLE | TUALATIN VALLEY FIRE & RESCUE
6 | 01040000201E6900000100000001010000008FE7EC0CDF06204146A2177BF1195341 | 65 | 26815 HIGHWAY 170 | | CANBY RFPD #62
7 | 01040000201E6900000100000001010000002FC149D18B392241CBD9801B12245341 | 253 | 87600 E GOVERNMENT CAMP LOOP | GOVERNMENT CAMP | HOODLAND RFPD #74
8 | 01040000201E690000010000000101000000B046D46B25882041D7EC8E183C3C5341 | 74 | 1520 NE 192ND AVE | GRESHAM | GRESHAM FIRE & EMERG SRVCS
9 | 01040000201E690000010000000101000000C668D7930D901F41350FEBB7F2335341 | 53 | 8480 SW SCHOLLS FERRY RD | BEAVERTON | TUALATIN VALLEY FIRE & RESCUE
10 | 01040000201E69000001000000010100000082EDAD44C9E91F417153C4A654345341 | 18 | 8720 SW 30TH AVE | PORTLAND | PORTLAND FIRE & RESCUE
SELECT id
, station
, address
, city
, st_astext(geom) geom
FROM fire_sta;
id | station | address | city | geom
----+---------+------------------------------+-----------------+-----------------------------------------------
1 | 50 | 12617 SW WALNUT ST | TIGARD | MULTIPOINT(515173.732354289 5030521.56357027)
2 | 211 | 4555 JEAN RD | LAKE OSWEGO | MULTIPOINT(521625.714787829 5027114.33621751)
3 | 11 | 102 E MAIN ST | GASTON | MULTIPOINT(489155.781155438 5031492.14882545)
4 | 26 | 5247 N LOMBARD ST | PORTLAND | MULTIPOINT(521640.147913175 5047662.91934105)
5 | 56 | 8455 SW ELLIGSEN RD | WILSONVILLE | MULTIPOINT(518508.254818878 5020313.24340334)
6 | 65 | 26815 HIGHWAY 170 | | MULTIPOINT(525167.525244938 5007301.9233175)
7 | 253 | 87600 E GOVERNMENT CAMP LOOP | GOVERNMENT CAMP | MULTIPOINT(597189.908765828 5017672.42973943)
8 | 74 | 1520 NE 192ND AVE | GRESHAM | MULTIPOINT(541714.710603913 5042416.38372346)
9 | 53 | 8480 SW SCHOLLS FERRY RD | BEAVERTON | MULTIPOINT(517123.394376409 5033930.87372189)
10 | 18 | 8720 SW 30TH AVE | PORTLAND | MULTIPOINT(522866.317069732 5034322.60573278)
Notice the projection of the data gives hard-to-read coordinates!
SELECT id
, station
, address
, city
, (ST_ASTEXT(ST_TRANSFORM(geom,4326))) geom
FROM fire_sta;
id | station | address | city | geom
----+---------+------------------------------+-----------------+------------------------------------------------
1 | 50 | 12617 SW WALNUT ST | TIGARD | MULTIPOINT(-122.806027592858 45.428051325105)
2 | 211 | 4555 JEAN RD | LAKE OSWEGO | MULTIPOINT(-122.723699509766 45.3972124570865)
3 | 11 | 102 E MAIN ST | GASTON | MULTIPOINT(-123.138647942155 45.4368683505389)
4 | 26 | 5247 N LOMBARD ST | PORTLAND | MULTIPOINT(-122.722608693335 45.5821707668332)
5 | 56 | 8455 SW ELLIGSEN RD | WILSONVILLE | MULTIPOINT(-122.763784232417 45.3360833873664)
6 | 65 | 26815 HIGHWAY 170 | | MULTIPOINT(-122.679455050874 45.2187565962888)
7 | 253 | 87600 E GOVERNMENT CAMP LOOP | GOVERNMENT CAMP | MULTIPOINT(-121.760251085664 45.3058277709189)
8 | 74 | 1520 NE 192ND AVE | GRESHAM | MULTIPOINT(-122.46574190944 45.5340339071676)
9 | 53 | 8480 SW SCHOLLS FERRY RD | BEAVERTON | MULTIPOINT(-122.780985680916 45.4586940268239)
10 | 18 | 8720 SW 30TH AVE | PORTLAND | MULTIPOINT(-122.707514263889 45.462055584884)
Here we have the Neighborhoods in the Portland Metro Area:
SELECT nbo_hood_id
, name
, ST_ASTEXT(ST_TRANSFORM(geom,4326))
FROM nbo_hood;
nbo_hood_id | name |
-------------+-------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
8 | LLOYD DISTRICT | MULTIPOLYGON(((-122.650480796952 45.5333589655569,-122.650510431462 45.5314726993377,-122.654567304327 45.531497367206,-122.654583838372 45.5293616414892,-122.653311176239 45.52876729323
211 | GRESHAM - NORTH GRESHAM | MULTIPOLYGON(((-122.460068956153 45.5591750869026,-122.460071836589 45.5581701759594,-122.460071474271 45.5581033059463,-122.459943494087 45.5581034443263,-122.459921124039 45.5581034656
96 | PARKROSE HEIGHTS | MULTIPOLYGON(((-122.537634551912 45.5337757045262,-122.537636878715 45.5335736333397,-122.537824367438 45.5335715422847,-122.545188064426 45.533490336749,-122.547000762669 45.53347027081
28 | NORTHFIELD | MULTIPOLYGON(((-122.497756840315 45.6083488130372,-122.495572604578 45.6083495710326,-122.495576108117 45.6101271754758,-122.493112204749 45.6101643722772,-122.49309425842 45.60917751321
287 | NORTH SALMON CREEK | MULTIPOLYGON(((-122.655676626356 45.7289193353581,-122.655477162054 45.7282245128809,-122.654758732645 45.7257234828009,-122.654380547849 45.7247388828086,-122.654560664795 45.7235452739
154 | CASCADE HIGHLANDS | MULTIPOLYGON(((-122.503939226142 45.6102082210104,-122.503923730676 45.6072320292005,-122.503908694318 45.6043453309102,-122.504661932669 45.6043443269167,-122.504804951482 45.6043411063
162 | FISHERS LANDING EAST | MULTIPOLYGON(((-122.495576108117 45.6101271754758,-122.495572604578 45.6083495710326,-122.497756840315 45.6083488130372,-122.497753173955 45.6066773378049,-122.492101178464 45.6066973944
350 | CHILDS | MULTIPOLYGON(((-122.712797021563 45.3873736667375,-122.71278317843 45.3862934000188,-122.712778032241 45.3859530356456,-122.712773822213 45.3856748658333,-122.712769287943 45.38535516713
147 | FOURTH PLAIN VILLAGE | MULTIPOLYGON(((-122.639197560032 45.6458942572195,-122.638937368859 45.6458664162537,-122.638389870996 45.6463356847865,-122.63783388227 45.646320363385,-122.637376439233 45.646147518485
316 | WEST HAZEL DELL | MULTIPOLYGON(((-122.714306015297 45.6971360464785,-122.713822245802 45.6969213359745,-122.706154919092 45.6970914144172,-122.703045332396 45.6971602242795,-122.702985164086 45.6971605011
I’ve cut off the entire polygon description.
Ok, so we can store geographic information… let’s put it to work!
Standard GIS operates with “tools”/“widgets”:
ArcGIS Toolbox
QGIS Toolbox
In PostGIS, we have functions (lots of them):
The next few examples are very basic! PostGIS is capable of highly advanced analytics, which we wont get into here.
Here we have a cohort of schools:
SELECT * FROM schools WHERE grade = '9-12';
Here we have a buffer for each (converting to different coordinate system):
SELECT ST_BUFFER(ST_TRANSFORM(geom,26910),2000)
FROM schools WHERE grade = '9-12';
Here we have a single ‘unioned’ buffer:
SELECT ST_UNION(ST_BUFFER(ST_TRANSFORM(geom,26910),2000))
FROM schools WHERE grade = '9-12';
One of the most common functions in GIS is a “spatial join”, wherein tables are joined based on geographic relation.
Let’s join the neighborhood name onto each school in the region!
As a reminder, here are the schools and neighborhoods:
SELECT s.*, n.name nbo_name
FROM schools s
JOIN nbo_hood n
ON ST_Intersects(s.geom,st_transform(n.geom,4326))
WHERE s.grade = '9-12';
Let’s get all of the Census Block Groups (CBGs) within Downtown Portland. There are many options for this:
SELECT c.fips
, c.pop10
, ST_Intersection(n.geom, c.geom) geom
FROM cbg c,
nbo_hood n
WHERE n.name = 'DOWNTOWN'
AND ST_Intersects(n.geom,c.geom);
SELECT c.*
FROM cbg c,
nbo_hood n
WHERE ST_INTERSECTS(c.geom, n.geom)
AND n.name = 'DOWNTOWN';
SELECT c.*
FROM cbg c,
nbo_hood n
WHERE ST_INTERSECTS(ST_CENTROID(c.geom), n.geom)
AND n.name = 'DOWNTOWN';
While we’re at it, lets also use ST_AREA to create a population density map!
SELECT fips
, pop10/(ST_AREA(c.geom)/1000) pop_density
, c.geom
FROM cbg c,
nbo_hood n
WHERE ST_INTERSECTS(ST_CENTROID(c.geom), n.geom)
AND n.name = 'DOWNTOWN';
These aren’t the only options! There are far more options such as:
Let’s find the top 10 longest trails in the Portland Metro Area!
SELECT systemname
, sum(st_length(geom))/5280 length
, ST_UNION(geom) -- merging them
FROM trails WHERE systemname IS NOT NULL
GROUP BY systemname ORDER BY length DESC LIMIT 10;
What is the closest fire station to Riverdale High?
SELECT f.*,
ST_DISTANCE(f.geom,ST_TRANSFORM(s.geom,26910)) dist
FROM fire_sta f,
(SELECT * FROM schools WHERE name = 'Riverdale High') s
ORDER BY dist
LIMIT 1;
id | station | st_astext | dist
----+---------+-----------------------------------------------+------------------
97 | 10 | MULTIPOINT(524763.806430033 5034486.74243309) | 1032.21244503901
Not bad, but we can do more!
WITH s AS
(SELECT ST_Transform(geom,26910) geom
FROM schools
WHERE name = 'Riverdale High')
SELECT ST_Transform(ST_Makeline(st_centroid(f.geom), ST_Centroid(s.geom)),4326)
FROM s,
(SELECT f.*, ST_DISTANCE(f.geom,s.geom) dist
FROM fire_sta f, s
ORDER BY dist
LIMIT 1) f;
We can use a LATERAL JOIN
to calculate all nearest neighbors!
WITH s AS (SELECT * FROM schools WHERE grade = '9-12')
SELECT ST_Transform(ST_Makeline(ST_Centroid(f.geom), ST_Centroid(ST_Transform(s.geom,26910))),4326)
FROM s CROSS JOIN LATERAL
(SELECT address, geom
FROM fire_sta ORDER BY ST_Transform(s.geom,26910) <#> geom LIMIT 1) f;
We can use distance to features to filter our cohort based on other features:
SELECT s.name
, ROUND(ST_Distance(ST_Transform(s.geom,2913),t.geom)) as dist
, s.geom
FROM schools s, trails t
WHERE ST_Distance(ST_Transform(s.geom,2913),t.geom) < 100;
Load in data from regional governments, vendors, or even Open Streetmaps!
However, we’ll need one more extension….
Like PostGIS, pgRouting is an extension that adds data types and functions for the handling of network analysis. Some cool functions are:
I primarily use OSM data because of this tool! It will parse raw OSM to networks with:
First, lets look at the roads data, which consists of Nodes and Edges. We’re mostly interested in the for visualization and nodes for analysis:
id | osm_id | osm_name | osm_meta | osm_source_id | osm_target_id | clazz | flags | source | target | km | kmh | cost | reverse_cost | x1 | y1 | x2 | y2 | geom
----+---------+-------------------------+----------+---------------+---------------+-------+-------+--------+--------+-----------+-----+-----------+--------------+--------------+------------+--------------+------------+--------
1 | 4440143 | Northeast Hodes Street | | 38756004 | 29936590 | 41 | 7 | 1 | 50 | 0.059913 | 40 | 0.0014978 | 0.0014978 | -122.8929526 | 45.5297334 | -122.8922158 | 45.5296458 | 0105000...
2 | 4440143 | Northeast Hodes Street | | 29936590 | 27286355 | 41 | 7 | 50 | 6 | 0.0115988 | 40 | 0.00029 | 0.00029 | -122.8922158 | 45.5296458 | -122.8920669 | 45.5296452 | 0105000...
3 | 4440143 | Northeast Hodes Street | | 27286355 | 29936591 | 41 | 7 | 6 | 54 | 0.0530484 | 40 | 0.0013262 | 0.0013262 | -122.8920669 | 45.5296452 | -122.8913859 | 45.5296425 | 0105000...
4 | 4440143 | Northeast Hodes Street | | 29936591 | 29936595 | 41 | 7 | 54 | 58 | 0.0657458 | 40 | 0.0016436 | 0.0016436 | -122.8913859 | 45.5296425 | -122.8905419 | 45.5296391 | 0105000...
5 | 4440143 | Northeast Hodes Street | | 29936595 | 27199860 | 41 | 7 | 58 | 13 | 0.0685188 | 40 | 0.001713 | 0.001713 | -122.8905419 | 45.5296391 | -122.8896623 | 45.5296356 | 0105000...
and target in the edges (above) links up with the node/intersection ID’s (below)!
id | clazz | osm_id | osm_name | ref_count | restrictions | geom_vertex
----+-------+----------+----------+-----------+--------------+---------------
1 | 0 | 38756004 | | 2 | | 010100002...
50 | 0 | 29936590 | | 1 | | 010100002...
6 | 0 | 27286355 | | 1 | | 010100002...
54 | 0 | 29936591 | | 1 | | 010100002...
58 | 0 | 29936595 | | 1 | | 010100002...
For this first route, let’s route between these targets: 45667 and 34045
SELECT * FROM pdx_roads_node WHERE id IN (92015,251821);
SELECT p.osm_name, x.cost, p.geom
FROM pdx_roads p join (
SELECT * FROM pgr_dijkstra(
'SELECT id, source, target, cost, reverse_cost FROM pdx_roads'
,92015
,251821
)
) x ON p.id = x.edge;
osm_name | cost | geom
-------------------------------------+-----------+-------------------
Southeast Helene Street | 0.001856 | 0105000020E6100000...
Southeast 58th Avenue | 0.0027587 | 0105000020E6100000...
Southeast Concorde Place | 0.0011906 | 0105000020E6100000...
Southeast Imlay Avenue | 0.0037218 | 0105000020E6100000...
Southeast Lois Street | 0.0059255 | 0105000020E6100000...
Southeast Lois Street | 0.0007229 | 0105000020E6100000...
Southeast Lois Street | 0.0021492 | 0105000020E6100000...
Southeast Lois Street | 0.0014191 | 0105000020E6100000...
Southeast Lois Street | 0.0026148 | 0105000020E6100000...
Southeast Lois Street | 0.002863 | 0105000020E6100000...
Southeast Lois Street | 0.0007276 | 0105000020E6100000...
Southeast Lois Street | 0.0009849 | 0105000020E6100000...
Southeast Lois Street | 0.0024699 | 0105000020E6100000...
Southeast Lois Street | 0.0011073 | 0105000020E6100000...
Southeast Lois Street | 0.001348 | 0105000020E6100000...
Southeast Lois Street | 0.0025831 | 0105000020E6100000...
Southeast Lois Street | 0.001708 | 0105000020E6100000...
Southeast Lois Street | 0.0015847 | 0105000020E6100000...
Southeast Lois Street | 0.0074722 | 0105000020E6100000...
Southeast Lois Street | 0.0019735 | 0105000020E6100000...
Southeast Lois Street | 0.0025121 | 0105000020E6100000...
Southwest Cornelius Pass Road | 0.0027643 | 0105000020E6100000...
Southwest Cornelius Pass Road | 0.0008814 | 0105000020E6100000...
Southwest Cornelius Pass Road | 0.0018234 | 0105000020E6100000...
Southeast Cornelius Pass Road | 0.0007404 | 0105000020E6100000...
Southwest Baseline Road | 0.0007444 | 0105000020E6100000...
Southwest Baseline Road | 0.0028978 | 0105000020E6100000...
Southwest Baseline Road | 0.0009953 | 0105000020E6100000...
Southwest Baseline Road | 0.0009114 | 0105000020E6100000...
Southwest Baseline Road | 0.0004179 | 0105000020E6100000...
Southwest Baseline Road | 0.0004582 | 0105000020E6100000...
Southwest Baseline Road | 0.0006979 | 0105000020E6100000...
Southwest Baseline Road | 0.0005732 | 0105000020E6100000...
Southwest Baseline Road | 0.0004901 | 0105000020E6100000...
Southwest Baseline Road | 0.0027577 | 0105000020E6100000...
Southwest Baseline Road | 0.0004096 | 0105000020E6100000...
Southwest Baseline Road | 0.0004396 | 0105000020E6100000...
Southwest Baseline Road | 0.0009592 | 0105000020E6100000...
Southwest Baseline Road | 0.00151 | 0105000020E6100000...
Southwest Baseline Road | 0.0011425 | 0105000020E6100000...
Southwest Baseline Road | 0.0028631 | 0105000020E6100000...
Southwest Baseline Road | 0.0007222 | 0105000020E6100000...
Southwest Baseline Road | 0.0028163 | 0105000020E6100000...
Southwest Baseline Road | 0.0005794 | 0105000020E6100000...
Southwest Baseline Road | 0.001034 | 0105000020E6100000...
Southwest Baseline Road | 0.0028915 | 0105000020E6100000...
Southwest Baseline Road | 0.0010045 | 0105000020E6100000...
Southwest Baseline Road | 0.0058289 | 0105000020E6100000...
Southwest Baseline Road | 0.0022246 | 0105000020E6100000...
Southwest Baseline Road | 0.0002601 | 0105000020E6100000...
Southwest Baseline Road | 0.0012276 | 0105000020E6100000...
Southwest Baseline Road | 0.0001367 | 0105000020E6100000...
Southwest Baseline Road | 0.0001642 | 0105000020E6100000...
Southwest Baseline Road | 0.0006795 | 0105000020E6100000...
And on, and on, and on…
We can even get stats about the ride:
WITH route as (SELECT p.osm_name, x.cost, st_transform(p.geom,26910) geom FROM pdx_roads p join (SELECT * FROM pgr_dijkstra('SELECT id, source, target, cost, reverse_cost FROM pdx_roads' ,92015,251821)) x ON p.id = x.edge)
SELECT SUM(ST_Length(geom)/1000) Total_km
, SUM(cost)*60 Total_Min
FROM route;
total_km | total_min
------------------+-----------
32.6837767909505 | 27.968016
We can route many-to-many if we’d like!
The following app shows commute flows based off of the LEHD LODES data set and OSM data.
What if we wanted to see everywhere we could drive within x minutes?
The “Driving Distance” tool gives us the ability to definite a starting point, and identify all points within a specified cost from it.
SELECT x.agg_cost as cost, x.node, x.edge, p.geom
FROM pdx_roads_node p join (
SELECT * FROM pgr_drivingdistance(
'SELECT id, source, target, cost, reverse_cost FROM pdx_roads'
,250376 -- start point, Downtown Portland
,(SELECT (SELECT 10.000) / (SELECT 60.000))) -- 10min
) x ON p.id = x.node;
What if we wanted to see everywhere we could walk within x minutes?
SELECT x.agg_cost as cost, x.node, x.edge, p.geom
FROM pdx_roads_node p join (
SELECT * FROM pgr_drivingdistance(
'SELECT id, source, target, km/5.00 as cost, reverse_cost FROM pdx_roads --!!!
WHERE clazz > 14' --!!! OSM has road types!
,250376 -- start point, Downtown Portland
,(SELECT (SELECT 10.000) / (SELECT 60.000))) -- 10min
) x ON p.id = x.node;
We don’t know which node we live by… so we can use our ‘nearest neighbor’ function to find it!
Using the following, it would be easy to create an application that created driving distances based on lat/lon (such as a click on a map):
SELECT x.agg_cost as cost, x.node, x.edge, p.geom
FROM pdx_roads_node p join (
SELECT * FROM pgr_drivingdistance(
'SELECT id, source, target, cost, reverse_cost FROM pdx_roads'
, (SELECT p.id
FROM pdx_roads_node p,
(SELECT ST_SetSRID(ST_MakePoint(-122.551,45.4668),4326) as geom) s
ORDER BY ST_DISTANCE(s.geom,p.geom)
LIMIT 1)
,(SELECT (SELECT 6.500) / (SELECT 60.000)))
) x ON p.id = x.node;
Things I didn’t create an example for:
I’m not going over the code to do this, but it’s from Abel Vázquez
Many of PostGIS’s functions work naitively with spatial indexes, which add to it’s incredible performance when compared to other tools.
Not just in terms of time saved by being an all-in-one-shop, but the actual performance vs. proprietary systems!
No more moving data between applications, servers, and storage: just run analyses where the data lives!
Allows teams to use the same data.
This seems like such a ‘duh’ comment, but in the GIS world it’s common to have multiple versions floating around multiple computers…
Though I didn’t provide examples, in a large enterprise it is great to ‘drive’ PostGIS from anywhere!
In fact, this entire presentation (including calling the queries and creating the maps) was made in R!