diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..0315c4f8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,33 @@ +If you encounter a bug in Pandana please: 1) first search the previously opened issues to see if the problem has already been reported; 2) If not, fill in the template below and tag with the appropriate `Type` label. You can delete any sections that do not apply. + +#### Description of the bug + + + + +#### Network data (optional) +If the issue is related to specific network data please provide a link to download the data or the function used to download the data. + + +#### Environment + +- Operating system: + +- Python version: + +- Pandana version: + +- Pandana required packages versions (optional): + + +#### Paste the code that reproduces the issue here: + +```python +# place code here +``` + + +#### Paste the error message (if applicable): +```python +# place error message here +``` \ No newline at end of file diff --git a/.gitignore b/.gitignore index fa0da1f0..32f57b41 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,9 @@ coverage.xml # Sphinx documentation docs/_build/ + +# OSMnet logs +logs + +# Example data +examples/data \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index d4428e3f..7d7d4812 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: python sudo: false python: - '2.7' +- '3.5' install: - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then wget http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh; @@ -17,10 +18,10 @@ install: - | conda create -q -c synthicity -n test-environment python=$TRAVIS_PYTHON_VERSION basemap matplotlib numpy pandas pip pytables requests - source activate test-environment -- pip install pytest-cov coveralls pep8 +- pip install pytest-cov coveralls pycodestyle osmnet - python setup.py install script: -- pep8 pandana +- pycodestyle pandana - python setup.py test --pytest-args "--cov pandana --cov-report term-missing" after_success: - coveralls diff --git a/LICENSE b/LICENSE index 722f25ef..dce41ce7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,8 @@ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 - Copyright (C) Autodesk + Copyright (C) UrbanSim Inc. + Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. diff --git a/README.rst b/README.rst index 2f5e8b87..c559ce51 100644 --- a/README.rst +++ b/README.rst @@ -9,8 +9,6 @@ Pandana :alt: Coverage Status :target: https://coveralls.io/r/UDST/pandana -A nice slideshow showing example code is available -`here `__. In this case, a picture is worth a thousand words. The image below shows the distance to the *2nd* nearest restaurant (rendered by matplotlib) @@ -40,8 +38,8 @@ queries are a more accurate representation of how people interact with their environment. We look forward to creative uses of a general library like this - please -let us know when you think you have a great use case with the hashtag -``#udst``. +let us know if you think you have a great use case by tweeting us at +``@urbansim`` or post on the UrbanSim `forum`_. Docs ---- @@ -56,9 +54,8 @@ Pandana is also available. Acknowledgments --------------- -None of this would be possible without the help of Dennis Luxen (now at -MapBox) and his OSRM (https://github.com/DennisOSRM/Project-OSRM). Thank -you Dennis! +None of this would be possible without the help of Dennis Luxen and +his OSRM (https://github.com/DennisOSRM/Project-OSRM). Thank you Dennis! Nearest neighbor queries are performed with the fastest k-d tree around, i.e. ANN (http://www.cs.umd.edu/~mount/ANN/). @@ -66,7 +63,17 @@ i.e. ANN (http://www.cs.umd.edu/~mount/ANN/). Academic Literature ------------------- -I'm currently working on getting a `complete description of the -methodology `__ -published in an academic journal. Please cite this paper when referring +A `complete description of the +methodology `__ +was presented at the Transportation Research Board Annual Conference in 2012. Please cite this paper when referring to the methodology implemented by this library. + +Related UDST libraries +---------------------- + +- `OSMnet`_ +- `UrbanAccess`_ + +.. _forum: http://discussion.urbansim.com/ +.. _OSMnet: https://github.com/udst/osmnet +.. _UrbanAccess: https://github.com/UDST/urbanaccess \ No newline at end of file diff --git a/conftest.py b/conftest.py index 6cb22e8c..b6fb046f 100644 --- a/conftest.py +++ b/conftest.py @@ -25,6 +25,6 @@ if os.environ.get('TRAVIS') == 'true': num_networks_tested = 1 else: - num_networks_tested = 5 + num_networks_tested = 6 pdna.reserve_num_graphs(num_networks_tested) diff --git a/docs/conf.py b/docs/conf.py index b95f7a98..c4c2f2fb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' numpydoc_show_class_members = False numpydoc_class_members_toctree = False @@ -43,33 +43,33 @@ source_suffix = '.rst' # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. -project = u'pandana' -copyright = u'2015, Autodesk' +project = 'pandana' +copyright = '2017, UrbanSim Inc.' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.1' +version = '0.3.0' # The full version, including alpha/beta/rc tags. -release = '0.1' +release = '0.3.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -77,27 +77,27 @@ # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # -- Options for HTML output ---------------------------------------------- @@ -110,26 +110,26 @@ # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -139,48 +139,48 @@ # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'pandanadoc' @@ -189,43 +189,42 @@ # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'pandana.tex', u'pandana Documentation', - u'Autodesk', 'manual'), + ('index', 'pandana.tex', 'pandana Documentation', 'UrbanSim Inc.', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- @@ -233,12 +232,12 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'pandana', u'pandana Documentation', - [u'Autodesk'], 1) + ('index', 'pandana', 'pandana Documentation', + ['UrbanSim Inc.'], 1) ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -247,19 +246,19 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'pandana', u'pandana Documentation', - u'Autodesk', 'pandana', 'One line description of project.', - 'Miscellaneous'), + ('index', 'pandana', 'pandana Documentation', + 'UrbanSim Inc.', 'pandana', 'One line description of project.', + 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False diff --git a/docs/futurework.rst b/docs/futurework.rst index fafefecb..80f0a8a0 100644 --- a/docs/futurework.rst +++ b/docs/futurework.rst @@ -14,11 +14,7 @@ includes: * Batch (multi-threaded) routing between a large number of pairs of nodes in the network -* Additional OpenStreetMap, ESRI ShapeFile and Geodatabase, - and GTFS (General Transit Feed Service) importing - -* Unification of multiple networks, like combining the pedestrian - OpenStreetMap network with the GTFS transit schedules +* Additional OpenStreetMap, ESRI ShapeFile and Geodatabase importing * Returning a DataFrame of all source-destination nodes within a certain distance (and including the distance) diff --git a/docs/index.rst b/docs/index.rst index 589d58d1..daaede70 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,6 +24,7 @@ Contents tutorial network loaders + utilities futurework Indices and tables diff --git a/docs/installation.rst b/docs/installation.rst index f857c07b..f078f77f 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -15,6 +15,7 @@ Pandana depends on the following libraries, most of which are in Anaconda: * `numpy`_ >= 1.8.0 * `pandas`_ >= 0.13.1 * `tables`_ >= 3.1.0 +* `osmnet`_ >= 0.1.0 Install the latest release -------------------------- @@ -29,7 +30,7 @@ conda ~~~~~ Pandana and some of its dependencies are hosted on -`Autodesks's Anaconda repository `__. +`UrbanSim Inc's Anaconda repository `__. To add this as a default installation channel for conda run this code in a terminal:: @@ -112,3 +113,4 @@ on your platform - for instance :code:`g++-mp-4.9` or :code:`g++-4.8`. .. _numpy: http://www.numpy.org/ .. _pandas: http://pandas.pydata.org/ .. _tables: http://www.pytables.org/ +.. _osmnet: http://github.com/udst/osmnet diff --git a/docs/introduction.rst b/docs/introduction.rst index 96f59c1a..782a5da3 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -13,7 +13,7 @@ Beyond simple access to destination queries, this library also implements more g we think network queries are a more accurate representation of how people interact with their environment. -We look forward to creative uses of a general library like this - please let us know when you think you have a great use case with the hashtag ``#udst``. +We look forward to creative uses of a general library like this - please let us know when you think you have a great use case by tweeting us at ``@urbansim`` or post on the UrbanSim `forum`_. The General Workflow ~~~~~~~~~~~~~~~~~~~~ @@ -95,6 +95,35 @@ variable using network queries. care, or urban predictive variables - e.g. average income in the local area, or simply for data exploration. A common use case will be to write to shapefiles and use in further GIS analysis, or to relate to parcels and - buildings and use in further analysis within UrbanSim and the Urban Data - Science Toolkit. There are many possibilities, and we hope designing a - flexible and easy to use engine will serve many use cases. + buildings and use in further analysis within `UrbanSim`_ and the `Urban Data Science Toolkit`_. + There are many possibilities, and we hope designing a flexible and easy to + use engine will serve many use cases. + + +Reporting bugs +~~~~~~~~~~~~~~~~~~~~~~~~ +Please report any bugs you encounter via `GitHub Issues `__. + +Contributing to Pandana +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +If you have improvements or new features you would like to see in Pandana: + +1. Open a feature request via `GitHub Issues `__. +2. Contribute your code from a fork or branch by using a Pull Request and request a review so it can be considered as an addition to the codebase. + +License +~~~~~~~~ + +Pandana is licensed under the AGPL license. + +Related UDST libraries +~~~~~~~~~~~~~~~~~~~~~~~~ + +- `OSMnet`_ +- `UrbanAccess`_ + +.. _forum: http://discussion.urbansim.com/ +.. _UrbanSim: https://github.com/UDST/urbansim +.. _Urban Data Science Toolkit: https://github.com/UDST +.. _OSMnet: https://github.com/udst/osmnet +.. _UrbanAccess: https://github.com/UDST/urbanaccess diff --git a/docs/loaders.rst b/docs/loaders.rst index c0e6e314..ff3a8ca2 100644 --- a/docs/loaders.rst +++ b/docs/loaders.rst @@ -11,16 +11,21 @@ OpenStreetMap ------------- A :py:class:`~pandana.network.Network` is created from OpenStreetMap using -the :py:func:`~pandana.loaders.osm.network_from_bbox` function:: +the :py:func:`~pandana.loaders.osm.pdna_network_from_bbox` function:: from pandana.loaders import osm - network = osm.network_from_bbox(37.859, -122.282, 37.881, -122.252) + network = osm.pdna_network_from_bbox(37.859, -122.282, 37.881, -122.252) By default the generated network contains only walkable routes, specify ``type='drive'`` to get driveable routes. These networks have one impedance set, named ``'distance'``, which is the distance between nodes in meters. +.. note:: + `pdna_network_from_bbox` uses the UDST library OSMnet to download and + process OpenStreetMap (OSM) street network data. Please see + the `OSMnet`_ repo for any OSM loader questions, bugs, or features. + The OSM API also includes the :py:func:`~pandana.loaders.osm.node_query` function for getting specific nodes within a bounding box. This can be used to populate a network with points of interest:: @@ -71,7 +76,7 @@ then exclude those nodes when saving to HDF5:: OpenStreetMap API ----------------- -.. autofunction:: pandana.loaders.osm.network_from_bbox +.. autofunction:: pandana.loaders.osm.pdna_network_from_bbox .. autofunction:: pandana.loaders.osm.node_query @@ -87,3 +92,5 @@ Pandas HDF5 API .. autofunction:: pandana.loaders.pandash5.network_to_pandas_hdf5 .. autofunction:: pandana.loaders.pandash5.network_from_pandas_hdf5 + +.. _OSMnet: https://github.com/udst/osmnet diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 84c2a84f..5f3c502f 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -7,8 +7,9 @@ in the ``Pandana`` repo which gives the entire workflow, but the discussion here will take things line-by-line with a sufficient summary of the functionality. -Note that these code samples assume you have imported pandana as follows:: +Note that these code samples assume you have imported pandas and pandana as follows:: + import pandas as pd import pandana as pdna Create the Network @@ -17,14 +18,14 @@ Create the Network First create the network. Although the API is incredibly simple, this is likely to be the most difficult part of using Pandana. In the future we will leverage the import functionality of tools like ``geopandas`` to -directly access OpenStreetMap and networks via shapefiles, +directly access networks via shapefiles, but for now the initialization :py:meth:`pandana.network.Network.__init__` takes a small number of Pandas Series objects. The network is comprised of a set of nodes and edges. We store our nodes and edges as two Pandas DataFrames in an HDFStore object. We can access them as follows (the demo data file can be -`downloaded here `__):: +`downloaded here `__):: store = pd.HDFStore('data/osm_bayarea.h5', "r") @@ -76,10 +77,10 @@ where 3000 meters is used as the horizon distance: :: net.precompute(3000) Note that a large amount of time is spent in the precomputations that take -place for these two lines of code. On my MacBook, these two lines of code +place for these two lines of code. On a MacBook, these two lines of code take 4 seconds and 8.5 seconds respectively. -**I also have a 4-core cpu, so if your precomputation is much slower, +**This was done on a 4-core cpu, so if your precomputation is much slower, check the IPython Notebook output (on the console) for a statement that says** ``Generating contraction hierarchies with 4 threads.`` **If your output says 1 instead of 4 you are running single threaded. If you are running on @@ -205,22 +206,27 @@ happen in the notebook) is also available. Note that these have a bounding box for reducing the display window. Although the underlying library is computing values for all nodes in the -region, it is extremely difficult to visualize this much data using -matplotlib. The GeoCanvas tool by Autodesk is expressly designed to join -indicators at the node level to shapes of parcels and produces a much more -professional output map. For quick interactive checking of results, +region, it is difficult to visualize this much data using +matplotlib. For quick interactive checking of results, the bounding box can be used to reduce the number of points that are shown, and sample code and images are included below. :: - bbox=[-122.539365,37.693047,-122.347698,37.816069] - net.plot(s, bbox=bbox, scheme="diverging", - color="BrBG", log_scale=True) + sf_bbox = [37.707794, -122.524338, 37.834192, -122.34993] + + net.plot(s, bbox=sf_bbox, + fig_kwargs={'figsize': [20, 20]}, + bmap_kwargs={'suppress_ticks': False, + 'resolution': 'h', 'epsg': '26943'}, + plot_kwargs={'cmap': 'BrBG', 's': 8, 'edgecolor': 'none'}) .. image:: img/500metersum.png :: - net.plot(u, bbox=bbox, scheme="diverging", - color="BrBG", log_scale=True) + net.plot(u, bbox=sf_bbox, + fig_kwargs={'figsize': [20, 20]}, + bmap_kwargs={'suppress_ticks': False, + 'resolution': 'h', 'epsg': '26943'}, + plot_kwargs={'cmap': 'BrBG', 's': 8, 'edgecolor': 'none'} .. image:: img/2000metersum.png diff --git a/docs/utilities.rst b/docs/utilities.rst new file mode 100644 index 00000000..8c719597 --- /dev/null +++ b/docs/utilities.rst @@ -0,0 +1,4 @@ +Utilities +========= + +.. autofunction:: pandana.utils.reindex diff --git a/examples/AnythingScore.ipynb b/examples/AnythingScore.ipynb deleted file mode 100644 index d4dd387a..00000000 --- a/examples/AnythingScore.ipynb +++ /dev/null @@ -1,162 +0,0 @@ -{ - "metadata": { - "name": "", - "signature": "sha256:52c5ec0c481a3b7ec7d72e43611bea87986cd8313542e0584cda9cc3b874edac" - }, - "nbformat": 3, - "nbformat_minor": 0, - "worksheets": [ - { - "cells": [ - { - "cell_type": "heading", - "level": 3, - "metadata": {}, - "source": [ - "Output has been removed from this notebook to reduce file sizes in the repo" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Before you get started, note that this notebook requires the [osm](https://github.com/geopandas/geopandas/tree/osm) branch of geopandas to be installed. This is all still fairly experimental functionality in geopandas, and thus should still be considered experimental here as well (`utils.py` is not fully unit tested). On the other hand, the actual POI queries in Pandana are unit tested and can be considered ready for release." - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "%load_ext autoreload\n", - "%autoreload 2\n", - "import pandas as pd\n", - "import numpy as np\n", - "import pandana as pdna\n", - "from pandana import utils\n", - "import geopandas.io.osm as osm\n", - "%matplotlib inline" - ], - "language": "python", - "metadata": {}, - "outputs": [] - }, - { - "cell_type": "heading", - "level": 2, - "metadata": {}, - "source": [ - "Read in the network and initialize" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "store = pd.HDFStore('osm_bayarea.h5', \"r\")\n", - "net=pdna.Network(store.nodes[\"x\"], \n", - " store.nodes[\"y\"], \n", - " store.edges[\"from\"], \n", - " store.edges[\"to\"], \n", - " store.edges[[\"weight\"]])\n", - "\n", - "# make sure you have enough categories for your score\n", - "net.init_pois(num_categories=10, max_dist=2400, max_pois=10)\n", - "\n", - "bbox = [-122.8662,37.1373,-121.4798,38.2158] # san francisco" - ], - "language": "python", - "metadata": {}, - "outputs": [] - }, - { - "cell_type": "heading", - "level": 2, - "metadata": {}, - "source": [ - "Make a decay function" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "from scipy.special import expit\n", - "def dist_to_contrib(dist):\n", - " dist = dist.astype('float') / 2400 # now varies from 0 to 1\n", - " dist = (dist * -10) + 5\n", - " return expit(dist)" - ], - "language": "python", - "metadata": {}, - "outputs": [] - }, - { - "cell_type": "heading", - "level": 2, - "metadata": {}, - "source": [ - "Create an anything score" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "s = utils.anything_score(\n", - " net,\n", - " {\n", - " \"shop=supermarket\": [3.0],\n", - " \"amenity=restaurant\": [.75, .45, .25, .25, .225, .225, .225, .225, .2, .2],\n", - " \"shop=convenience\": [.5, .45, .4, .35, .3],\n", - " \"amenity=cafe\": [1.25, .75],\n", - " \"amenity=bank\": [1.0],\n", - " \"leisure=park\": [1.0],\n", - " \"amenity=school\": [1.0],\n", - " \"amenity=library\": [1.0],\n", - " \"amenity=bar\": [1.0]\n", - " },\n", - " 2400,\n", - " dist_to_contrib, # decay function to apply to distance\n", - " bbox\n", - ")\n", - "print s.describe() " - ], - "language": "python", - "metadata": {}, - "outputs": [] - }, - { - "cell_type": "heading", - "level": 2, - "metadata": {}, - "source": [ - "See it" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "from IPython.display import set_matplotlib_formats\n", - "set_matplotlib_formats('png')\n", - "out_bbox = utils.bbox_convert([-122.539365,37.693047,-122.347698,37.816069], \n", - " from_epsg=4326, to_epsg=3740)\n", - "net.plot(s, bbox=out_bbox, scheme=\"diverging\", color=\"BrBG\")" - ], - "language": "python", - "metadata": {}, - "outputs": [] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [], - "language": "python", - "metadata": {}, - "outputs": [] - } - ], - "metadata": {} - } - ] -} \ No newline at end of file diff --git a/examples/Example.ipynb b/examples/Example.ipynb index fe6e3436..4c2acad3 100644 --- a/examples/Example.ipynb +++ b/examples/Example.ipynb @@ -1,576 +1,590 @@ { + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "## Output has been removed from this notebook to reduce file sizes in the repo" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "import pandas as pd\n", + "import numpy as np\n", + "import pandana as pdna\n", + "from pandana.loaders import osm\n", + "%matplotlib inline\n", + "\n", + "import warnings\n", + "warnings.filterwarnings('ignore')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Download OpenStreetMap restaurants for a good part of the Bay Area" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "###### Note: used http://boundingbox.klokantech.com/ to get the bounding box" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "# Bounding box from link above\n", + "tmp = [-122.8662, 37.1373, -121.4798, 38.2158]\n", + "\n", + "# Reordered for Pandana functions\n", + "bbox = [tmp[1], tmp[0], tmp[3], tmp[2]]\n", + "\n", + "poi_df = osm.node_query(*bbox, tags='amenity=restaurant')\n", + "x, y = poi_df['lon'], poi_df['lat']" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Get previously stored OpenStreetMap networks for Bay Area\n", + "\n", + "Download the data here: https://s3-us-west-1.amazonaws.com/synthpop-data2/pandana/osm_bayarea.h5" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "store = pd.HDFStore('data/osm_bayarea.h5', \"r\")\n", + "nodes = store.nodes\n", + "edges = store.edges\n", + "print nodes.head(3)\n", + "print edges.head(3)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Initialize and preprocess the network" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "net=pdna.Network(nodes.x, \n", + " nodes.y, \n", + " edges[\"from\"], \n", + " edges[\"to\"],\n", + " edges[[\"weight\"]])\n", + "net.precompute(3000)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Nearest *point-of-interest* queries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "net.init_pois(num_categories=1, max_dist=2000, max_pois=10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "net.set_pois(\"restaurants\", x, y)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "a = net.nearest_pois(2000, \"restaurants\", num_pois=10)\n", + "print a.head(1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Here's a map of the distance to the nearest restaurant" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "deletable": true, + "editable": true, + "scrolled": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "fig_kwargs = {'figsize': [20, 20]}\n", + "bmap_kwargs = {'suppress_ticks': False, 'resolution': 'h', 'epsg': '26943'}\n", + "plot_kwargs = {'cmap': 'BrBG', 's': 8, 'edgecolor': 'none'}\n", + "\n", + "sf_tmp = [-122.524338, 37.707794, -122.34993, 37.834192]\n", + "sf_bbox = [sf_tmp[1], sf_tmp[0], sf_tmp[3], sf_tmp[2]]\n", + "\n", + "net.plot(a[1], bbox=sf_bbox, \n", + " fig_kwargs=fig_kwargs, bmap_kwargs=bmap_kwargs, plot_kwargs=plot_kwargs)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Here's a map of the distance to the 5th nearest restaurant" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "net.plot(a[5], bbox=sf_bbox, \n", + " fig_kwargs=fig_kwargs, bmap_kwargs=bmap_kwargs, plot_kwargs=plot_kwargs)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Here's a map of the distance to the 10th nearest restaurant" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "net.plot(a[10], bbox=sf_bbox, \n", + " fig_kwargs=fig_kwargs, bmap_kwargs=bmap_kwargs, plot_kwargs=plot_kwargs)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# A similar workflow is used to do general network aggregations" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Relate the x-ys to nodes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "node_ids = net.get_node_ids(x, y)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "subslide" + } + }, + "source": [ + "## Assign the variable (in this case just location) to the network" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "net.set(node_ids)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "subslide" + } + }, + "source": [ + "## This is it - run the queries!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "%time s = net.aggregate(500, type=\"sum\", decay=\"linear\")\n", + "%time t = net.aggregate(1000, type=\"sum\", decay=\"linear\")\n", + "%time u = net.aggregate(2000, type=\"sum\", decay=\"linear\")\n", + "%time v = net.aggregate(3000, type=\"sum\", decay=\"linear\")\n", + "%time w = net.aggregate(3000, type=\"count\", decay=\"flat\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Here's a map of access to restaurants with a 500m radius" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "net.plot(s, bbox=sf_bbox, \n", + " fig_kwargs=fig_kwargs, bmap_kwargs=bmap_kwargs, plot_kwargs=plot_kwargs)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Or 1000 meters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "net.plot(t, bbox=sf_bbox, \n", + " fig_kwargs=fig_kwargs, bmap_kwargs=bmap_kwargs, plot_kwargs=plot_kwargs)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Or 2000 meters radius" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "net.plot(u, bbox=sf_bbox, \n", + " fig_kwargs=fig_kwargs, bmap_kwargs=bmap_kwargs, plot_kwargs=plot_kwargs)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Or 3000m radius" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "net.plot(v, bbox=sf_bbox, \n", + " fig_kwargs=fig_kwargs, bmap_kwargs=bmap_kwargs, plot_kwargs=plot_kwargs)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Or the whole Bay Area region" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "deletable": true, + "editable": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "net.plot(w, bbox=bbox, \n", + " fig_kwargs=fig_kwargs, bmap_kwargs=bmap_kwargs, plot_kwargs=plot_kwargs)" + ] + } + ], "metadata": { - "name": "", - "signature": "sha256:b761890efa92ab55edfbf2425340e80dee066f572c7a58c1b0c359d2d9def445" - }, - "nbformat": 3, - "nbformat_minor": 0, - "worksheets": [ - { - "cells": [ - { - "cell_type": "heading", - "level": 2, - "metadata": { - "slideshow": { - "slide_type": "skip" - } - }, - "source": [ - "Output has been removed from this notebook to reduce file sizes in the repo" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "%load_ext autoreload\n", - "%autoreload 2\n", - "import pandas as pd\n", - "import numpy as np\n", - "import pandana as pdna\n", - "import geopandas.io.osm as osm\n", - "%matplotlib inline" - ], - "language": "python", - "metadata": { - "slideshow": { - "slide_type": "skip" - } - }, - "outputs": [] - }, - { - "cell_type": "heading", - "level": 2, - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "Download OpenStreetMap restaurants for a good part of the Bay Area" - ] - }, - { - "cell_type": "heading", - "level": 6, - "metadata": { - "slideshow": { - "slide_type": "fragment" - } - }, - "source": [ - "Note: used http://boundingbox.klokantech.com/ to get the bounding box" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "gdf = osm.query_osm('node', \n", - " bbox=[-122.8662,37.1373,-121.4798,38.2158],\n", - " tags='amenity=restaurant')" - ], - "language": "python", - "metadata": { - "slideshow": { - "slide_type": "fragment" - } - }, - "outputs": [] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "gdf = gdf[gdf.type == 'Point'].to_crs(epsg=3740)\n", - "print gdf.geometry.head(3)\n", - "print len(gdf)" - ], - "language": "python", - "metadata": { - "slideshow": { - "slide_type": "fragment" - } - }, - "outputs": [] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "x, y = zip(*[(p.x, p.y) for (i, p) \n", - " in gdf.geometry.iteritems()])\n", - "x = pd.Series(x)\n", - "y = pd.Series(y)" - ], - "language": "python", - "metadata": { - "slideshow": { - "slide_type": "fragment" - } - }, - "outputs": [] - }, - { - "cell_type": "heading", - "level": 2, - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "Get OpenStreetMap networks for Bay Area that I had previously - someday soon we'll have direct OSM import" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "store = pd.HDFStore('data/osm_bayarea.h5', \"r\")\n", - "nodes = store.nodes\n", - "edges = store.edges\n", - "print nodes.head(3)\n", - "print edges.head(3)" - ], - "language": "python", - "metadata": { - "slideshow": { - "slide_type": "fragment" - } - }, - "outputs": [] - }, - { - "cell_type": "heading", - "level": 2, - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "Initialize and preprocess the network" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "net=pdna.Network(nodes.x, \n", - " nodes.y, \n", - " edges[\"from\"], \n", - " edges.to, \n", - " edges[[\"weight\"]])\n", - "net.precompute(3000)" - ], - "language": "python", - "metadata": { - "slideshow": { - "slide_type": "fragment" - } - }, - "outputs": [] - }, - { - "cell_type": "heading", - "level": 2, - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "Nearest *point-of-interest* queries" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "net.init_pois(num_categories=1, max_dist=2000, max_pois=10)" - ], - "language": "python", - "metadata": { - "slideshow": { - "slide_type": "fragment" - } - }, - "outputs": [] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "net.set_pois(\"restaurants\", x, y)" - ], - "language": "python", - "metadata": { - "slideshow": { - "slide_type": "fragment" - } - }, - "outputs": [] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "a = net.nearest_pois(2000, \"restaurants\", num_pois=10)\n", - "print a.head(1)" - ], - "language": "python", - "metadata": { - "slideshow": { - "slide_type": "fragment" - } - }, - "outputs": [] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "from shapely.geometry import Point\n", - "from fiona.crs import from_epsg\n", - "import geopandas as gpd\n", - "bbox=[-122.539365,37.693047,-122.347698,37.816069]\n", - "bbox = gpd.GeoSeries([Point(bbox[0], bbox[1]),\n", - " Point(bbox[2], bbox[3])], \n", - " crs=from_epsg(4326))\n", - "bbox = bbox.to_crs(epsg=3740)\n", - "bbox = [bbox[0].x, bbox[0].y, bbox[1].x, bbox[1].y]" - ], - "language": "python", - "metadata": { - "slideshow": { - "slide_type": "skip" - } - }, - "outputs": [] - }, - { - "cell_type": "heading", - "level": 2, - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "Here's a map of the distance to the nearest restaurant" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "net.plot(a[1], bbox=bbox, scheme=\"diverging\", \n", - " color=\"BrBG\")" - ], - "language": "python", - "metadata": { - "slideshow": { - "slide_type": "fragment" - } - }, - "outputs": [] - }, - { - "cell_type": "heading", - "level": 2, - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "Here's a map of the distance to the 5th nearest restaurant" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "net.plot(a[5], bbox=bbox, scheme=\"diverging\", \n", - " color=\"BrBG\")" - ], - "language": "python", - "metadata": { - "slideshow": { - "slide_type": "fragment" - } - }, - "outputs": [] - }, - { - "cell_type": "heading", - "level": 2, - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "Here's a map of the distance to the 10th nearest restaurant" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "net.plot(a[10], bbox=bbox, scheme=\"diverging\", \n", - " color=\"BrBG\")" - ], - "language": "python", - "metadata": { - "slideshow": { - "slide_type": "fragment" - } - }, - "outputs": [] - }, - { - "cell_type": "heading", - "level": 1, - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "A similar workflow is used to do general network aggregations" - ] - }, - { - "cell_type": "heading", - "level": 2, - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "Relate the x-ys to nodes" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "node_ids = net.get_node_ids(x, y)" - ], - "language": "python", - "metadata": { - "slideshow": { - "slide_type": "fragment" - } - }, - "outputs": [] - }, - { - "cell_type": "heading", - "level": 2, - "metadata": { - "slideshow": { - "slide_type": "subslide" - } - }, - "source": [ - "Assign the variable (in this case just location) to the network" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "net.set(node_ids)" - ], - "language": "python", - "metadata": { - "slideshow": { - "slide_type": "fragment" - } - }, - "outputs": [] - }, - { - "cell_type": "heading", - "level": 2, - "metadata": { - "slideshow": { - "slide_type": "subslide" - } - }, - "source": [ - "This is it - run the queries!" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "%time s = net.aggregate(500, type=\"sum\", decay=\"linear\")\n", - "%time t = net.aggregate(1000, type=\"sum\", decay=\"linear\")\n", - "%time u = net.aggregate(2000, type=\"sum\", decay=\"linear\")\n", - "%time v = net.aggregate(3000, type=\"sum\", decay=\"linear\")\n", - "%time w = net.aggregate(3000, type=\"count\", decay=\"flat\")" - ], - "language": "python", - "metadata": { - "slideshow": { - "slide_type": "fragment" - } - }, - "outputs": [] - }, - { - "cell_type": "heading", - "level": 2, - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "Here's a map of access to restaurants with a 500m radius" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "net.plot(s, bbox=bbox, scheme=\"diverging\", \n", - " color=\"BrBG\", log_scale=True)" - ], - "language": "python", - "metadata": { - "slideshow": { - "slide_type": "fragment" - } - }, - "outputs": [] - }, - { - "cell_type": "heading", - "level": 2, - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "Or 1000 meters" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "net.plot(t, bbox=bbox, scheme=\"diverging\", \n", - " color=\"BrBG\", log_scale=True)" - ], - "language": "python", - "metadata": { - "slideshow": { - "slide_type": "fragment" - } - }, - "outputs": [] - }, - { - "cell_type": "heading", - "level": 2, - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "Or 2000 meters radius" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "net.plot(u, bbox=bbox, scheme=\"diverging\", \n", - " color=\"BrBG\", log_scale=True)" - ], - "language": "python", - "metadata": { - "slideshow": { - "slide_type": "fragment" - } - }, - "outputs": [] - }, - { - "cell_type": "heading", - "level": 2, - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "Or 3000m radius" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "net.plot(v, bbox=bbox, scheme=\"diverging\", \n", - " color=\"BrBG\", log_scale=True)" - ], - "language": "python", - "metadata": { - "slideshow": { - "slide_type": "fragment" - } - }, - "outputs": [] - }, - { - "cell_type": "heading", - "level": 2, - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "Or the whole Bay Area region - someone please help me with this visualization!" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "net.plot(v, scheme=\"diverging\", \n", - " color=\"BrBG\", log_scale=True)" - ], - "language": "python", - "metadata": { - "slideshow": { - "slide_type": "fragment" - } - }, - "outputs": [] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [], - "language": "python", - "metadata": {}, - "outputs": [] - } - ], - "metadata": {} + "kernelspec": { + "display_name": "Python 2", + "language": "python", + "name": "python2" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.13" } - ] -} \ No newline at end of file + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/ez_setup.py b/ez_setup.py index a34fa806..90b93f68 100644 --- a/ez_setup.py +++ b/ez_setup.py @@ -39,6 +39,7 @@ DEFAULT_VERSION = "5.7" DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/" + def _python_cmd(*args): """ Return True if the command succeeded. @@ -130,7 +131,7 @@ def _do_download(version, download_base, to_dir, download_delay): def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, - to_dir=os.curdir, download_delay=15): + to_dir=os.curdir, download_delay=15): to_dir = os.path.abspath(to_dir) rep_modules = 'pkg_resources', 'setuptools' imported = set(sys.modules).intersection(rep_modules) @@ -160,6 +161,7 @@ def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, del pkg_resources, sys.modules['pkg_resources'] return _do_download(version, download_base, to_dir, download_delay) + def _clean_check(cmd, target): """ Run the command to download target. If the command fails, clean up before @@ -172,6 +174,7 @@ def _clean_check(cmd, target): os.unlink(target) raise + def download_file_powershell(url, target): """ Download the file at url to target using Powershell (which will validate @@ -191,6 +194,7 @@ def download_file_powershell(url, target): ] _clean_check(cmd, target) + def has_powershell(): if platform.system() != 'Windows': return False @@ -202,12 +206,15 @@ def has_powershell(): return False return True + download_file_powershell.viable = has_powershell + def download_file_curl(url, target): cmd = ['curl', url, '--silent', '--output', target] _clean_check(cmd, target) + def has_curl(): cmd = ['curl', '--version'] with open(os.path.devnull, 'wb') as devnull: @@ -217,12 +224,15 @@ def has_curl(): return False return True + download_file_curl.viable = has_curl + def download_file_wget(url, target): cmd = ['wget', url, '--quiet', '--output-document', target] _clean_check(cmd, target) + def has_wget(): cmd = ['wget', '--version'] with open(os.path.devnull, 'wb') as devnull: @@ -232,8 +242,10 @@ def has_wget(): return False return True + download_file_wget.viable = has_wget + def download_file_insecure(url, target): """ Use Python to download the file, even though it cannot authenticate the @@ -250,8 +262,10 @@ def download_file_insecure(url, target): with open(target, "wb") as dst: dst.write(data) + download_file_insecure.viable = lambda: True + def get_best_downloader(): downloaders = ( download_file_powershell, @@ -262,8 +276,10 @@ def get_best_downloader(): viable_downloaders = (dl for dl in downloaders if dl.viable()) return next(viable_downloaders, None) + def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, - to_dir=os.curdir, delay=15, downloader_factory=get_best_downloader): + to_dir=os.curdir, delay=15, + downloader_factory=get_best_downloader): """ Download setuptools from a specified location and return its filename @@ -287,12 +303,14 @@ def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, downloader(url, saveto) return os.path.realpath(saveto) + def _build_install_args(options): """ Build the arguments to 'python setup.py install' on the setuptools package """ return ['--user'] if options.user_install else [] + def _parse_args(): """ Parse the command line for options @@ -318,6 +336,7 @@ def _parse_args(): # positional arguments are ignored return options + def main(): """Install or upgrade setuptools and EasyInstall""" options = _parse_args() @@ -328,5 +347,6 @@ def main(): ) return _install(archive, _build_install_args(options)) + if __name__ == '__main__': sys.exit(main()) diff --git a/pandana/__init__.py b/pandana/__init__.py index c98279e4..1d894360 100644 --- a/pandana/__init__.py +++ b/pandana/__init__.py @@ -1,3 +1,3 @@ -from network import Network +from .network import Network -version = __version__ = '0.2dev' +version = __version__ = '0.3.0' diff --git a/pandana/loaders/osm.py b/pandana/loaders/osm.py index 3d5af822..d9de9def 100644 --- a/pandana/loaders/osm.py +++ b/pandana/loaders/osm.py @@ -1,88 +1,61 @@ """ -Tools for creating Pandana networks from Open Street Map. +Tools for creating Pandana networks from OpenStreetMap. """ -from itertools import islice, izip import pandas as pd import requests +from osmnet.load import network_from_bbox from .. import Network -from ..utils import great_circle_dist as gcd - -uninteresting_tags = { - 'source', - 'source_ref', - 'source:ref', - 'history', - 'attribution', - 'created_by', - 'tiger:tlid', - 'tiger:upload_uuid', -} - - -def build_network_osm_query( - lat_min, lng_min, lat_max, lng_max, network_type='walk'): + + +def pdna_network_from_bbox( + lat_min=None, lng_min=None, lat_max=None, lng_max=None, bbox=None, + network_type='walk', two_way=True, + timeout=180, memory=None, max_query_area_size=50 * 1000 * 50 * 1000): """ - Construct an OSM way query for a bounding box. + Make a Pandana network from a bounding lat/lon box + request to the Overpass API. Distance will be in the default units meters. Parameters ---------- lat_min, lng_min, lat_max, lng_max : float + bbox : tuple + Bounding box formatted as a 4 element tuple: + (lng_max, lat_min, lng_min, lat_max) network_type : {'walk', 'drive'}, optional Specify whether the network will be used for walking or driving. A value of 'walk' attempts to exclude things like freeways, while a value of 'drive' attempts to exclude things like bike and walking paths. + two_way : bool, optional + Whether the routes are two-way. If True, node pairs will only + occur once. + timeout : int, optional + the timeout interval for requests and to pass to Overpass API + memory : int, optional + server memory allocation size for the query, in bytes. + If none, server will use its default allocation size + max_query_area_size : float, optional + max area for any part of the geometry, in the units the geometry is in Returns ------- - query : str - - """ - query_fmt = ( - '[out:json];' - '(' - ' way' - ' ["highway"]' - ' {filters}' - ' ({lat_min},{lng_min},{lat_max},{lng_max});' - ' >;' # the '>' makes it recurse so we get ways and way nodes - ');' - 'out;') - - if network_type == 'walk': - filters = '["highway"!~"motor"]' - elif network_type == 'drive': - filters = '["highway"!~"foot|cycle"]' - else: - raise ValueError('Invalid network_type argument') - - return query_fmt.format( - lat_min=lat_min, lng_min=lng_min, lat_max=lat_max, lng_max=lng_max, - filters=filters) - + network : pandana.Network -def make_osm_query(query): """ - Make a request to OSM and return the parsed JSON. - Parameters - ---------- - query : str - A string in the Overpass QL format. + nodes, edges = network_from_bbox(lat_min=lat_min, lng_min=lng_min, + lat_max=lat_max, lng_max=lng_max, + bbox=bbox, network_type=network_type, + two_way=two_way, timeout=timeout, + memory=memory, + max_query_area_size=max_query_area_size) - Returns - ------- - data : dict - - """ - osm_url = 'http://www.overpass-api.de/api/interpreter' - req = requests.get(osm_url, params={'data': query}) - req.raise_for_status() - - return req.json() + return Network( + nodes['x'], nodes['y'], + edges['from'], edges['to'], edges[['distance']]) def process_node(e): @@ -93,12 +66,22 @@ def process_node(e): Parameters ---------- e : dict - Returns ------- node : dict - """ + + uninteresting_tags = { + 'source', + 'source_ref', + 'source:ref', + 'history', + 'attribution', + 'created_by', + 'tiger:tlid', + 'tiger:upload_uuid', + } + node = { 'id': e['id'], 'lat': e['lat'], @@ -106,220 +89,32 @@ def process_node(e): } if 'tags' in e: - for t, v in e['tags'].items(): + for t, v in list(e['tags'].items()): if t not in uninteresting_tags: node[t] = v return node -def process_way(e): +def make_osm_query(query): """ - Process a way element entry into a list of dicts suitable for going into - a Pandas DataFrame. + Make a request to OSM and return the parsed JSON. Parameters ---------- - e : dict + query : str + A string in the Overpass QL format. Returns ------- - way : dict - waynodes : list of dict - - """ - way = { - 'id': e['id'] - } - - if 'tags' in e: - for t, v in e['tags'].items(): - if t not in uninteresting_tags: - way[t] = v - - waynodes = [] - - for n in e['nodes']: - waynodes.append({'way_id': e['id'], 'node_id': n}) - - return way, waynodes - - -def parse_network_osm_query(data): - """ - Convert OSM query data to DataFrames of ways and way-nodes. - - Parameters - ---------- data : dict - Result of an OSM query. - - Returns - ------- - nodes, ways, waynodes : pandas.DataFrame """ - if len(data['elements']) == 0: - raise RuntimeError('OSM query results contain no data.') - - nodes = [] - ways = [] - waynodes = [] - - for e in data['elements']: - if e['type'] == 'node': - nodes.append(process_node(e)) - elif e['type'] == 'way': - w, wn = process_way(e) - ways.append(w) - waynodes.extend(wn) - - return ( - pd.DataFrame.from_records(nodes, index='id'), - pd.DataFrame.from_records(ways, index='id'), - pd.DataFrame.from_records(waynodes, index='way_id')) - - -def ways_in_bbox(lat_min, lng_min, lat_max, lng_max, network_type='walk'): - """ - Get DataFrames of OSM data in a bounding box. - - Parameters - ---------- - lat_min, lng_min, lat_max, lng_max : float - network_type : {'walk', 'drive'}, optional - Specify whether the network will be used for walking or driving. - A value of 'walk' attempts to exclude things like freeways, - while a value of 'drive' attempts to exclude things like - bike and walking paths. - - Returns - ------- - nodes, ways, waynodes : pandas.DataFrame - - """ - return parse_network_osm_query(make_osm_query(build_network_osm_query( - lat_min, lng_min, lat_max, lng_max, network_type=network_type))) - - -def intersection_nodes(waynodes): - """ - Returns a set of all the nodes that appear in 2 or more ways. - - Parameters - ---------- - waynodes : pandas.DataFrame - Mapping of way IDs to node IDs as returned by `ways_in_bbox`. - - Returns - ------- - intersections : set - Node IDs that appear in 2 or more ways. - - """ - counts = waynodes.node_id.value_counts() - return set(counts[counts > 1].index.values) - - -def node_pairs(nodes, ways, waynodes, two_way=True): - """ - Create a table of node pairs with the distances between them. - - Parameters - ---------- - nodes : pandas.DataFrame - Must have 'lat' and 'lon' columns. - ways : pandas.DataFrame - Table of way metadata. - waynodes : pandas.DataFrame - Table linking way IDs to node IDs. Way IDs should be in the index, - with a column called 'node_ids'. - two_way : bool, optional - Whether the routes are two-way. If True, node pairs will only - occur once. - - Returns - ------- - pairs : pandas.DataFrame - Will have columns of 'from_id', 'to_id', and 'distance'. - The index will be a MultiIndex of (from id, to id). - The distance metric is in meters. - - """ - def pairwise(l): - return izip(islice(l, 0, len(l)), islice(l, 1, None)) - intersections = intersection_nodes(waynodes) - waymap = waynodes.groupby(level=0, sort=False) - pairs = [] - - for id, row in ways.iterrows(): - nodes_in_way = waymap.get_group(id).node_id.values - nodes_in_way = filter(lambda x: x in intersections, nodes_in_way) - - if len(nodes_in_way) < 2: - # no nodes to connect in this way - continue - - for from_node, to_node in pairwise(nodes_in_way): - fn = nodes.loc[from_node] - tn = nodes.loc[to_node] - - distance = gcd(fn.lat, fn.lon, tn.lat, tn.lon) - - pairs.append({ - 'from_id': from_node, - 'to_id': to_node, - 'distance': distance - }) - - if not two_way: - pairs.append({ - 'from_id': to_node, - 'to_id': from_node, - 'distance': distance - }) - - pairs = pd.DataFrame.from_records(pairs) - pairs.index = pd.MultiIndex.from_arrays( - [pairs['from_id'].values, pairs['to_id'].values]) - - return pairs - - -def network_from_bbox( - lat_min, lng_min, lat_max, lng_max, network_type='walk', two_way=True): - """ - Make a Pandana network from a bounding lat/lon box. - - Parameters - ---------- - lat_min, lng_min, lat_max, lng_max : float - network_type : {'walk', 'drive'}, optional - Specify whether the network will be used for walking or driving. - A value of 'walk' attempts to exclude things like freeways, - while a value of 'drive' attempts to exclude things like - bike and walking paths. - two_way : bool, optional - Whether the routes are two-way. If True, node pairs will only - occur once. - - Returns - ------- - network : pandana.Network - - """ - nodes, ways, waynodes = ways_in_bbox( - lat_min, lng_min, lat_max, lng_max, network_type) - pairs = node_pairs(nodes, ways, waynodes, two_way=two_way) - - # make the unique set of nodes that ended up in pairs - node_ids = sorted( - set(pairs['from_id'].unique()).union(set(pairs['to_id'].unique()))) - nodes = nodes.loc[node_ids] + osm_url = 'http://www.overpass-api.de/api/interpreter' + req = requests.get(osm_url, params={'data': query}) + req.raise_for_status() - return Network( - nodes['lon'], nodes['lat'], - pairs['from_id'], pairs['to_id'], pairs[['distance']]) + return req.json() def build_node_query(lat_min, lng_min, lat_max, lng_max, tags=None): diff --git a/pandana/loaders/tests/test_osm.py b/pandana/loaders/tests/test_osm.py index 76444b60..a2859e74 100644 --- a/pandana/loaders/tests/test_osm.py +++ b/pandana/loaders/tests/test_osm.py @@ -1,5 +1,3 @@ -import numpy.testing as npt -import pandas.util.testing as pdt import pytest import pandana @@ -21,43 +19,6 @@ def bbox2(): return 37.8668405874, -122.2590948685, 37.8679028054, -122.2586363885 -@pytest.fixture(scope='module') -def bbox3(): - # West Berkeley including highway 80, frontage roads, and foot paths - # Sample query: http://overpass-turbo.eu/s/6VE - return ( - 37.85225504880375, -122.30295896530151, - 37.85776128099243, - 122.2954273223877) - - -@pytest.fixture(scope='module') -def query_data1(bbox1): - return osm.make_osm_query(osm.build_network_osm_query(*bbox1)) - - -@pytest.fixture(scope='module') -def query_data2(bbox2): - return osm.make_osm_query(osm.build_network_osm_query(*bbox2)) - - -@pytest.fixture(scope='module') -def dataframes1(query_data1): - return osm.parse_network_osm_query(query_data1) - - -@pytest.fixture(scope='module') -def dataframes2(query_data2): - return osm.parse_network_osm_query(query_data2) - - -def test_make_osm_query(query_data1): - assert isinstance(query_data1, dict) - assert len(query_data1['elements']) == 24 - assert len( - [e for e in query_data1['elements'] if e['type'] == 'node']) == 22 - assert len([e for e in query_data1['elements'] if e['type'] == 'way']) == 2 - - def test_process_node(): test_node = { 'id': 'id', @@ -80,130 +41,9 @@ def test_process_node(): assert osm.process_node(test_node) == expected -def test_process_way(): - test_way = { - "type": "way", - "id": 188434143, - "timestamp": "2014-01-04T22:18:14Z", - "version": 2, - "changeset": 19814115, - "user": "dchiles", - "uid": 153669, - "nodes": [ - 53020977, - 53041093, - ], - "tags": { - 'source': 'source', - "addr:city": "Berkeley", - "highway": "secondary", - "name": "Telegraph Avenue", - } - } - - expected_way = { - 'id': test_way['id'], - 'addr:city': test_way['tags']['addr:city'], - 'highway': test_way['tags']['highway'], - 'name': test_way['tags']['name'] - } - - expected_waynodes = [ - {'way_id': test_way['id'], 'node_id': test_way['nodes'][0]}, - {'way_id': test_way['id'], 'node_id': test_way['nodes'][1]} - ] - - way, waynodes = osm.process_way(test_way) - - assert way == expected_way - assert waynodes == expected_waynodes - - -def test_parse_network_osm_query(dataframes1): - nodes, ways, waynodes = dataframes1 - - assert len(nodes) == 22 - assert len(ways) == 2 - assert len(waynodes.index.unique()) == 2 - - -def test_parse_network_osm_query_raises(): - data = osm.make_osm_query(osm.build_network_osm_query( - 37.8, -122.252, 37.8, -122.252)) - with pytest.raises(RuntimeError): - osm.parse_network_osm_query(data) - - -def test_ways_in_bbox(bbox1, dataframes1): - nodes, ways, waynodes = osm.ways_in_bbox(*bbox1) - exp_nodes, exp_ways, exp_waynodes = dataframes1 - - pdt.assert_frame_equal(nodes, exp_nodes) - pdt.assert_frame_equal(ways, exp_ways) - pdt.assert_frame_equal(waynodes, exp_waynodes) - - -@pytest.mark.parametrize( - 'network_type, noset', - [('walk', {'motorway', 'motorway_link'}), - ('drive', {'footway', 'cycleway'})]) -def test_ways_in_bbox_walk_network(bbox3, network_type, noset): - nodes, ways, waynodes = osm.ways_in_bbox(*bbox3, network_type=network_type) - - for _, way in ways.iterrows(): - assert way['highway'] not in noset - - -def test_intersection_nodes1(dataframes1): - _, _, waynodes = dataframes1 - intersections = osm.intersection_nodes(waynodes) - - assert intersections == {53041093} - - -def test_intersection_nodes2(dataframes2): - _, _, waynodes = dataframes2 - intersections = osm.intersection_nodes(waynodes) - - assert intersections == {53099275, 53063555} - - -def test_node_pairs_two_way(dataframes2): - nodes, ways, waynodes = dataframes2 - pairs = osm.node_pairs(nodes, ways, waynodes) - - assert len(pairs) == 1 - - fn = 53063555 - tn = 53099275 - - pair = pairs.loc[(fn, tn)] - - assert pair.from_id == fn - assert pair.to_id == tn - npt.assert_allclose(pair.distance, 101.20535797547758) - - -def test_node_pairs_one_way(dataframes2): - nodes, ways, waynodes = dataframes2 - pairs = osm.node_pairs(nodes, ways, waynodes, two_way=False) - - assert len(pairs) == 2 - - n1 = 53063555 - n2 = 53099275 - - for p1, p2 in [(n1, n2), (n2, n1)]: - pair = pairs.loc[(p1, p2)] - - assert pair.from_id == p1 - assert pair.to_id == p2 - npt.assert_allclose(pair.distance, 101.20535797547758) - - @skipiftravis def test_network_from_bbox(bbox2): - net = osm.network_from_bbox(*bbox2) + net = osm.pdna_network_from_bbox(*bbox2) assert isinstance(net, pandana.Network) diff --git a/pandana/loaders/tests/test_pandash5.py b/pandana/loaders/tests/test_pandash5.py index aac3443e..140c00e8 100644 --- a/pandana/loaders/tests/test_pandash5.py +++ b/pandana/loaders/tests/test_pandash5.py @@ -34,7 +34,7 @@ def impedance_names(): def edge_weights(edges, impedance_names): return pd.DataFrame( {impedance_names[0]: [1] * len(edges), - impedance_names[1]: range(1, len(edges) + 1)}) + impedance_names[1]: list(range(1, len(edges) + 1))}) @pytest.fixture(scope='module') diff --git a/pandana/network.py b/pandana/network.py index 614fa641..20e38689 100644 --- a/pandana/network.py +++ b/pandana/network.py @@ -62,12 +62,11 @@ def reserve_num_graphs(num): class Network: """ Create the transportation network in the city. Typical data would be - distance based from OpenStreetMap or possibly using transit data from - GTFS. + distance based from OpenStreetMap or travel time from GTFS transit data. Parameters ---------- - node_x : Pandas Series, flaot + node_x : Pandas Series, float Defines the x attribute for nodes in the network (e.g. longitude) node_y : Pandas Series, float Defines the y attribute for nodes in the network (e.g. latitude) @@ -83,10 +82,16 @@ class Network: Specifies one or more *impedances* on the network which define the distances between nodes. Multiple impedances can be used to capture travel times at different times of day, for instance - two_way : boolean, optional + twoway : boolean, optional Whether the edges in this network are two way edges or one way ( where the one direction is directed from the from node to the to - node) + node). If twoway = True, it is assumed that the from and to id in the + edge table occurs once and that travel can occur in both directions + on the single edge record. Pandana will internally flip and append + the from and to ids to the original edges to create a two direction + network. If twoway = False, it is assumed that travel can only occur + in the explicit direction indicated by the from and to id in the edge + table. """ @@ -227,7 +232,7 @@ def set(self, node_ids, variable=None, name="tmp"): Parameters ---------- - node_id : Pandas Series, int + node_ids : Pandas Series, int A series of node_ids which are usually computed using get_node_ids on this object. variable : Pandas Series, float, optional @@ -240,7 +245,10 @@ def set(self, node_ids, variable=None, name="tmp"): not actually used). If variable is not set, then it is assumed that the variable is all "ones" at the location specified by node_ids. This could be, for instance, the location of all - coffee shops which don't really have a variable to aggregate. + coffee shops which don't really have a variable to aggregate. The + variable is connected to the closest node in the Pandana network + which assumes no impedance between the location of the variable + and the location of the closest network node. name : string, optional Name the variable. This is optional in the sense that if you don't specify it, the default name will be used. Since the same @@ -259,13 +267,13 @@ def set(self, node_ids, variable=None, name="tmp"): df = pd.DataFrame({name: variable, "node_idx": self._node_indexes(node_ids)}) - l = len(df) + length = len(df) df = df.dropna(how="any") newl = len(df) - if l-newl > 0: + if length-newl > 0: print( "Removed %d rows because they contain missing values" % - (l-newl)) + (length-newl)) if name not in self.variable_names: self.variable_names.append(name) @@ -286,7 +294,9 @@ def precompute(self, distance): Parameters ---------- distance : float - The maximum distance to use + The maximum distance to use. This will usually be a distance unit + in meters however if you have customized the impedance this could + be in other units such as utility or time etc. Returns ------- @@ -317,7 +327,11 @@ def aggregate(self, distance, type="sum", decay="linear", imp_name=None, Parameters ---------- distance : float - The maximum distance to aggregate data within + The maximum distance to aggregate data within. 'distance' can + represent any impedance unit that you have set as your edge + weight. This will usually be a distance unit in meters however + if you have customized the impedance this could be in other + units such as utility or time etc. type : string The type of aggregation, can be one of "ave", "sum", "std", "count", and now "min", "25pct", "median", "75pct", and "max" will @@ -382,8 +396,11 @@ def get_node_ids(self, x_col, y_col, mapping_distance=-1): location of dataset. x_col and y_col should use the same index. mapping_distance : float, optional The maximum distance that will be considered a match between the - x, y data and the nearest node in the network. If not specified, - every x, y coordinate will be mapped to the nearest node + x, y data and the nearest node in the network. This will usually + be a distance unit in meters however if you have customized the + impedance this could be in other units such as utility or time + etc. If not specified, every x, y coordinate will be mapped to + the nearest node. Returns ------- @@ -422,7 +439,7 @@ def plot( cbar_kwargs=None): """ Plot an array of data on a map using matplotlib and Basemap, - automatically matching the data to node positions. + automatically matching the data to the Pandana network node positions. Keyword arguments are passed to the plotting routine. @@ -498,7 +515,10 @@ def init_pois(self, num_categories, max_dist, max_pois): num_categories : int Number of categories of POIs max_dist : float - Maximum distance that will be tested to nearest POIs + Maximum distance that will be tested to nearest POIs. This will + usually be a distance unit in meters however if you have + customized the impedance this could be in other + units such as utility or time etc. max_pois : Maximum number of POIs to return in the nearest query @@ -513,11 +533,14 @@ def init_pois(self, num_categories, max_dist, max_pois): self.num_poi_categories = num_categories self.max_pois = max_pois - _pyaccess.initialize_pois(num_categories, max_dist, max_pois) + _pyaccess.initialize_pois(num_categories, max_dist, max_pois, self.graph_no) def set_pois(self, category, x_col, y_col): """ - Set the location of all the pois of this category + Set the location of all the pois of this category. The pois are + connected to the closest node in the Pandana network which assumes + no impedance between the location of the variable and the location + of the closest network node. Parameters ---------- @@ -546,7 +569,7 @@ def set_pois(self, category, x_col, y_col): self.poi_category_indexes[category] = xys.index _pyaccess.initialize_category(self.poi_category_names.index(category), - xys.astype('float32')) + xys.astype('float32'), self.graph_no) def nearest_pois(self, distance, category, num_pois=1, max_distance=None, imp_name=None, include_poi_ids=False): @@ -557,7 +580,10 @@ def nearest_pois(self, distance, category, num_pois=1, max_distance=None, Parameters ---------- distance : float - The maximum distance to look for pois + The maximum distance to look for pois. This will usually be a + distance unit in meters however if you have customized the + impedance this could be in other units such as utility or time + etc. category : string The name of the category of poi to look for num_pois : int @@ -565,7 +591,10 @@ def nearest_pois(self, distance, category, num_pois=1, max_distance=None, columns in the DataFrame that gets returned max_distance : float, optional The value to set the distance to if there is NO poi within the - specified distance - if not specified, gets set to distance + specified distance - if not specified, gets set to distance. This + will usually be a distance unit in meters however if you have + customized the impedance this could be in other units such as + utility or time etc. imp_name : string, optional The impedance name to use for the aggregation on this network. Must be one of the impedance names passed in the constructor of @@ -613,7 +642,7 @@ def nearest_pois(self, distance, category, num_pois=1, max_distance=None, 0) a[a == -1] = max_distance df = pd.DataFrame(a, index=self.node_ids) - df.columns = range(1, num_pois+1) + df.columns = list(range(1, num_pois+1)) if include_poi_ids: b = _pyaccess.find_all_nearest_pois(distance, @@ -633,7 +662,7 @@ def nearest_pois(self, distance, category, num_pois=1, max_distance=None, # initialized as a pandas series - this really is pandas-like # thinking. it's complicated on the inside, but quite # intuitive to the user I think - s = df2[col] + s = df2[col].astype('int') df2[col] = self.poi_category_indexes[category].values[s] df2[col][s == -1] = np.nan @@ -649,7 +678,10 @@ def low_connectivity_nodes(self, impedance, count, imp_name=None): Parameters ---------- impedance : float - Distance within which to search for other connected nodes. + Distance within which to search for other connected nodes. This + will usually be a distance unit in meters however if you have + customized the impedance this could be in other units such as + utility or time etc. count : int Threshold for connectivity. If a node is connected to fewer than this many nodes within `impedance` it will be identified diff --git a/pandana/tests/test_great_circle_dist.py b/pandana/tests/test_great_circle_dist.py deleted file mode 100644 index c34b35b7..00000000 --- a/pandana/tests/test_great_circle_dist.py +++ /dev/null @@ -1,16 +0,0 @@ -import numpy.testing as npt - -from pandana.utils import great_circle_dist as gcd - - -def test_gcd(): - # tested against geopy - # https://geopy.readthedocs.org/en/latest/#module-geopy.distance - lat1 = 41.49008 - lon1 = -71.312796 - lat2 = 41.499498 - lon2 = -81.695391 - - expected = 864456.76162966 - - npt.assert_allclose(gcd(lat1, lon1, lat2, lon2), expected) diff --git a/pandana/tests/test_pandana.py b/pandana/tests/test_pandana.py index 73b6bf7b..d06def97 100644 --- a/pandana/tests/test_pandana.py +++ b/pandana/tests/test_pandana.py @@ -5,6 +5,7 @@ import pandas as pd import pytest from pandas.util import testing as pdt +from pandana.testing import skipiftravis import pandana.network as pdna @@ -33,6 +34,24 @@ def fin(): return net +# initialize a second network +@pytest.fixture(scope="module") +def second_sample_osm(request): + store = pd.HDFStore( + os.path.join(os.path.dirname(__file__), 'osm_sample.h5'), "r") + nodes, edges = store.nodes, store.edges + net = pdna.Network(nodes.x, nodes.y, edges["from"], edges.to, + edges[["weight"]]) + + net.precompute(2000) + + def fin(): + store.close() + request.addfinalizer(fin) + + return net + + def random_node_ids(net, ssize): return pd.Series(np.random.choice(net.node_ids, ssize)) @@ -229,8 +248,6 @@ def test_pois(sample_osm): with pytest.raises(AssertionError): net.nearest_pois(2000, "restaurants", num_pois=11) - -def test_pois_indexes(sample_osm): net = sample_osm x, y = random_x_y(sample_osm, 100) x.index = ['lab%d' % i for i in range(len(x))] @@ -240,3 +257,19 @@ def test_pois_indexes(sample_osm): d = net.nearest_pois(2000, "restaurants", num_pois=10, include_poi_ids=True) + + +@skipiftravis +def test_pois2(second_sample_osm): + net2 = second_sample_osm + + ssize = 50 + np.random.seed(0) + x, y = random_x_y(second_sample_osm, ssize) + + # make sure poi searches work on second graph + net2.init_pois(num_categories=1, max_dist=2000, max_pois=10) + + net2.set_pois("restaurants", x, y) + + print(net2.nearest_pois(2000, "restaurants", num_pois=10)) diff --git a/pandana/tests/test_utils.py b/pandana/tests/test_utils.py new file mode 100644 index 00000000..d6e4b3ec --- /dev/null +++ b/pandana/tests/test_utils.py @@ -0,0 +1,35 @@ +import pandas as pd +import pytest + +from pandana.utils import reindex + + +@pytest.fixture +def node_df(): + data = {'node_id': [44, 55, 33, 22, 11], + 'x': [-122, -123, -124, -125, -126], + 'y': [37, 38, 39, 40, 41]} + index = [1, 2, 3, 4, 5] + + df = pd.DataFrame(data, index) + return df + + +@pytest.fixture +def result_series(): + data = {'value': [10, 20, 30, 40, 50]} + index = [11, 22, 33, 44, 55] + df = pd.DataFrame(data, index) + df.index.name = 'id' + s = pd.Series(df.value, df.index) + return s + + +def test_reindex(node_df, result_series): + + reindexed_results = pd.DataFrame({'value': reindex(result_series, + node_df.node_id)}) + + assert len(reindexed_results) == 5 + assert reindexed_results['value'][1] == 40 + assert reindexed_results['value'][5] == 10 diff --git a/pandana/utils.py b/pandana/utils.py index 97086573..10e0885d 100644 --- a/pandana/utils.py +++ b/pandana/utils.py @@ -1,38 +1,27 @@ -from __future__ import division +import pandas as pd -import math - -def great_circle_dist(lat1, lon1, lat2, lon2): +def reindex(series1, series2): """ - Get the distance (in meters) between two lat/lon points - via the Haversine formula. + Reindex the first series by the second series. Parameters ---------- - lat1, lon1, lat2, lon2 : float - Latitude and longitude in degrees. + series1 : pandas.Series + Pandas series to reindex + series2 : pandas.Series + Pandas series to set the index of series1 by Returns ------- - dist : float - Distance in meters. - + df.right : pandas.DataFrame """ - radius = 6372795 # meters - - lat1 = math.radians(lat1) - lon1 = math.radians(lon1) - lat2 = math.radians(lat2) - lon2 = math.radians(lon2) - - dlat = lat2 - lat1 - dlon = lon2 - lon1 - - # formula from: - # http://en.wikipedia.org/wiki/Haversine_formula#The_haversine_formula - a = math.pow(math.sin(dlat / 2), 2) - b = math.cos(lat1) * math.cos(lat2) * math.pow(math.sin(dlon / 2), 2) - d = 2 * radius * math.asin(math.sqrt(a + b)) - return d + # this function is identical to the reindex function found in UrbanSim in + # urbansim/utils/misc.py + df = pd.merge(pd.DataFrame({"left": series2}), + pd.DataFrame({"right": series1}), + left_on="left", + right_index=True, + how="left") + return df.right diff --git a/setup.py b/setup.py index f9eabf36..3ac97e2e 100644 --- a/setup.py +++ b/setup.py @@ -29,6 +29,7 @@ def run_tests(self): errno = pytest.main(self.pytest_args or '') sys.exit(errno) + include_dirs = [ np.get_include(), '.', @@ -90,7 +91,7 @@ def run_tests(self): if mac_ver >= [10, 9]: extra_compile_args += ['-D NO_TR1_MEMORY'] -version = '0.2dev' +version = '0.3.0' # read long description from README with open('README.rst', 'r') as f: @@ -99,7 +100,7 @@ def run_tests(self): setup( packages=packages, name='pandana', - author='Autodesk', + author='UrbanSim Inc.', version=version, license='AGPL', description=('Pandas Network Analysis - ' @@ -120,13 +121,15 @@ def run_tests(self): 'numpy>=1.8.0', 'pandas>=0.13.1', 'requests>=2.0', - 'tables>=3.1.0' + 'tables>=3.1.0', + 'osmnet>=0.1.2', ], tests_require=['pytest'], cmdclass={'test': PyTest}, classifiers=[ 'Development Status :: 3 - Alpha', 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.5', 'License :: OSI Approved :: GNU Affero General Public License v3' ], ) diff --git a/src/pyaccesswrap.cpp b/src/pyaccesswrap.cpp index 268d77c7..498dbd6a 100644 --- a/src/pyaccesswrap.cpp +++ b/src/pyaccesswrap.cpp @@ -84,11 +84,11 @@ create_graph(PyObject *self, PyObject *args) static PyObject * initialize_pois(PyObject *self, PyObject *args) { - int nc, mi; + int nc, mi, gno; double md; - if (!PyArg_ParseTuple(args, "idi", &nc, &md, &mi)) return NULL; + if (!PyArg_ParseTuple(args, "idii", &nc, &md, &mi, &gno)) return NULL; - std::shared_ptr sa = sas[0]; + std::shared_ptr sa = sas[gno]; sa->initializePOIs(nc,md,mi); @@ -99,11 +99,11 @@ initialize_pois(PyObject *self, PyObject *args) static PyObject * initialize_category(PyObject *self, PyObject *args) { - int id; + int id, gno; PyObject *input1; - if (!PyArg_ParseTuple(args, "iO", &id, &input1)) return NULL; + if (!PyArg_ParseTuple(args, "iOi", &id, &input1, &gno)) return NULL; - std::shared_ptr sa = sas[0]; + std::shared_ptr sa = sas[gno]; PyArrayObject *pyo; pyo = (PyArrayObject*)PyArray_ContiguousFromObject(input1, @@ -532,11 +532,67 @@ static PyMethodDef myMethods[] = { }; -PyMODINIT_FUNC init_pyaccess(void) +struct module_state { + PyObject *error; +}; + +#if PY_MAJOR_VERSION >= 3 +#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) +#else +#define GETSTATE(m) (&_state) +static struct module_state _state; +#endif + +#if PY_MAJOR_VERSION >= 3 +static PyObject * +error_out(PyObject *m) { + struct module_state *st = GETSTATE(m); + PyErr_SetString(st->error, "something bad happened"); + return NULL; +} + +static int pyaccess_traverse(PyObject *m, visitproc visit, void *arg) { + Py_VISIT(GETSTATE(m)->error); + return 0; +} + +static int pyaccess_clear(PyObject *m) { + Py_CLEAR(GETSTATE(m)->error); + return 0; +} + +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_pyaccess", + NULL, + sizeof(struct module_state), + myMethods, + NULL, + pyaccess_traverse, + pyaccess_clear, + NULL +}; +#endif + + +PyMODINIT_FUNC +#if PY_MAJOR_VERSION >= 3 +PyInit__pyaccess(void) +#else +init_pyaccess(void) +#endif { +#if PY_MAJOR_VERSION >= 3 + PyObject *m = PyModule_Create(&moduledef); +#else PyObject *m=Py_InitModule("_pyaccess", myMethods); +#endif import_array(); PyObject *pyError = PyErr_NewException((char*)"pyaccess.error", NULL, NULL); - Py_INCREF(pyError); - PyModule_AddObject(m, "error", pyError); + Py_INCREF(pyError); + PyModule_AddObject(m, "error", pyError); + +#if PY_MAJOR_VERSION >= 3 + return m; +#endif }