started work on backend
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
# pylint: disable-msg=W0614,W0401,W0611,W0622
|
||||
|
||||
# flake8: noqa
|
||||
|
||||
__docformat__ = 'restructuredtext'
|
||||
|
||||
# Let users know if they're missing any of our hard dependencies
|
||||
hard_dependencies = ("numpy", "pytz", "dateutil")
|
||||
missing_dependencies = []
|
||||
|
||||
for dependency in hard_dependencies:
|
||||
try:
|
||||
__import__(dependency)
|
||||
except ImportError as e:
|
||||
missing_dependencies.append(dependency)
|
||||
|
||||
if missing_dependencies:
|
||||
raise ImportError(
|
||||
"Missing required dependencies {0}".format(missing_dependencies))
|
||||
del hard_dependencies, dependency, missing_dependencies
|
||||
|
||||
# numpy compat
|
||||
from pandas.compat.numpy import *
|
||||
|
||||
try:
|
||||
from pandas._libs import (hashtable as _hashtable,
|
||||
lib as _lib,
|
||||
tslib as _tslib)
|
||||
except ImportError as e: # pragma: no cover
|
||||
# hack but overkill to use re
|
||||
module = str(e).replace('cannot import name ', '')
|
||||
raise ImportError("C extension: {0} not built. If you want to import "
|
||||
"pandas from the source directory, you may need to run "
|
||||
"'python setup.py build_ext --inplace --force' to build "
|
||||
"the C extensions first.".format(module))
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
# let init-time option registration happen
|
||||
import pandas.core.config_init
|
||||
|
||||
from pandas.core.api import *
|
||||
from pandas.core.sparse.api import *
|
||||
from pandas.tseries.api import *
|
||||
from pandas.core.computation.api import *
|
||||
from pandas.core.reshape.api import *
|
||||
|
||||
# deprecate tools.plotting, plot_params and scatter_matrix on the top namespace
|
||||
import pandas.tools.plotting
|
||||
plot_params = pandas.plotting._style._Options(deprecated=True)
|
||||
# do not import deprecate to top namespace
|
||||
scatter_matrix = pandas.util._decorators.deprecate(
|
||||
'pandas.scatter_matrix', pandas.plotting.scatter_matrix, '0.20.0',
|
||||
'pandas.plotting.scatter_matrix')
|
||||
|
||||
from pandas.util._print_versions import show_versions
|
||||
from pandas.io.api import *
|
||||
from pandas.util._tester import test
|
||||
import pandas.testing
|
||||
|
||||
# extension module deprecations
|
||||
from pandas.util._depr_module import _DeprecatedModule
|
||||
|
||||
json = _DeprecatedModule(deprmod='pandas.json',
|
||||
moved={'dumps': 'pandas.io.json.dumps',
|
||||
'loads': 'pandas.io.json.loads'})
|
||||
parser = _DeprecatedModule(deprmod='pandas.parser',
|
||||
removals=['na_values'],
|
||||
moved={'CParserError': 'pandas.errors.ParserError'})
|
||||
lib = _DeprecatedModule(deprmod='pandas.lib', deprmodto=False,
|
||||
moved={'Timestamp': 'pandas.Timestamp',
|
||||
'Timedelta': 'pandas.Timedelta',
|
||||
'NaT': 'pandas.NaT',
|
||||
'infer_dtype': 'pandas.api.types.infer_dtype'})
|
||||
tslib = _DeprecatedModule(deprmod='pandas.tslib',
|
||||
moved={'Timestamp': 'pandas.Timestamp',
|
||||
'Timedelta': 'pandas.Timedelta',
|
||||
'NaT': 'pandas.NaT',
|
||||
'NaTType': 'type(pandas.NaT)',
|
||||
'OutOfBoundsDatetime': 'pandas.errors.OutOfBoundsDatetime'})
|
||||
|
||||
# use the closest tagged version if possible
|
||||
from ._version import get_versions
|
||||
v = get_versions()
|
||||
__version__ = v.get('closest-tag', v['version'])
|
||||
del get_versions, v
|
||||
|
||||
# module level doc-string
|
||||
__doc__ = """
|
||||
pandas - a powerful data analysis and manipulation library for Python
|
||||
=====================================================================
|
||||
|
||||
**pandas** is a Python package providing fast, flexible, and expressive data
|
||||
structures designed to make working with "relational" or "labeled" data both
|
||||
easy and intuitive. It aims to be the fundamental high-level building block for
|
||||
doing practical, **real world** data analysis in Python. Additionally, it has
|
||||
the broader goal of becoming **the most powerful and flexible open source data
|
||||
analysis / manipulation tool available in any language**. It is already well on
|
||||
its way toward this goal.
|
||||
|
||||
Main Features
|
||||
-------------
|
||||
Here are just a few of the things that pandas does well:
|
||||
|
||||
- Easy handling of missing data in floating point as well as non-floating
|
||||
point data.
|
||||
- Size mutability: columns can be inserted and deleted from DataFrame and
|
||||
higher dimensional objects
|
||||
- Automatic and explicit data alignment: objects can be explicitly aligned
|
||||
to a set of labels, or the user can simply ignore the labels and let
|
||||
`Series`, `DataFrame`, etc. automatically align the data for you in
|
||||
computations.
|
||||
- Powerful, flexible group by functionality to perform split-apply-combine
|
||||
operations on data sets, for both aggregating and transforming data.
|
||||
- Make it easy to convert ragged, differently-indexed data in other Python
|
||||
and NumPy data structures into DataFrame objects.
|
||||
- Intelligent label-based slicing, fancy indexing, and subsetting of large
|
||||
data sets.
|
||||
- Intuitive merging and joining data sets.
|
||||
- Flexible reshaping and pivoting of data sets.
|
||||
- Hierarchical labeling of axes (possible to have multiple labels per tick).
|
||||
- Robust IO tools for loading data from flat files (CSV and delimited),
|
||||
Excel files, databases, and saving/loading data from the ultrafast HDF5
|
||||
format.
|
||||
- Time series-specific functionality: date range generation and frequency
|
||||
conversion, moving window statistics, moving window linear regressions,
|
||||
date shifting and lagging, etc.
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# flake8: noqa
|
||||
|
||||
from .tslib import iNaT, NaT, Timestamp, Timedelta, OutOfBoundsDatetime
|
||||
|
||||
# TODO
|
||||
# period is directly dependent on tslib and imports python
|
||||
# modules, so exposing Period as an alias is currently not possible
|
||||
# from period import Period
|
||||
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# cython: profile=False
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
BIN
Binary file not shown.
backend/venv/lib/python3.6/site-packages/pandas/_libs/tslibs/nattype.cpython-36m-x86_64-linux-gnu.so
Executable
BIN
Binary file not shown.
BIN
Binary file not shown.
backend/venv/lib/python3.6/site-packages/pandas/_libs/tslibs/offsets.cpython-36m-x86_64-linux-gnu.so
Executable
BIN
Binary file not shown.
backend/venv/lib/python3.6/site-packages/pandas/_libs/tslibs/parsing.cpython-36m-x86_64-linux-gnu.so
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -0,0 +1,23 @@
|
||||
|
||||
# This file was generated by 'versioneer.py' (0.15) from
|
||||
# revision-control system data, or from the parent directory name of an
|
||||
# unpacked source archive. Distribution tarballs contain a pre-generated copy
|
||||
# of this file.
|
||||
|
||||
from warnings import catch_warnings
|
||||
with catch_warnings(record=True):
|
||||
import json
|
||||
import sys
|
||||
|
||||
version_json = '''
|
||||
{
|
||||
"dirty": false,
|
||||
"error": null,
|
||||
"full-revisionid": "0409521665bd436a10aea7e06336066bf07ff057",
|
||||
"version": "0.23.4"
|
||||
}
|
||||
''' # END VERSION_JSON
|
||||
|
||||
|
||||
def get_versions():
|
||||
return json.loads(version_json)
|
||||
@@ -0,0 +1,2 @@
|
||||
""" public toolkit API """
|
||||
from . import types, extensions # noqa
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
"""Public API for extending panadas objects."""
|
||||
from pandas.core.accessor import (register_dataframe_accessor, # noqa
|
||||
register_index_accessor,
|
||||
register_series_accessor)
|
||||
from pandas.core.algorithms import take # noqa
|
||||
from pandas.core.arrays.base import ExtensionArray # noqa
|
||||
from pandas.core.dtypes.dtypes import ExtensionDtype # noqa
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
""" public toolkit API """
|
||||
|
||||
from pandas.core.dtypes.api import * # noqa
|
||||
from pandas.core.dtypes.dtypes import (CategoricalDtype, # noqa
|
||||
DatetimeTZDtype,
|
||||
PeriodDtype,
|
||||
IntervalDtype)
|
||||
from pandas.core.dtypes.concat import union_categoricals # noqa
|
||||
from pandas._libs.lib import infer_dtype # noqa
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,454 @@
|
||||
"""
|
||||
compat
|
||||
======
|
||||
|
||||
Cross-compatible functions for Python 2 and 3.
|
||||
|
||||
Key items to import for 2/3 compatible code:
|
||||
* iterators: range(), map(), zip(), filter(), reduce()
|
||||
* lists: lrange(), lmap(), lzip(), lfilter()
|
||||
* unicode: u() [no unicode builtin in Python 3]
|
||||
* longs: long (int in Python 3)
|
||||
* callable
|
||||
* iterable method compatibility: iteritems, iterkeys, itervalues
|
||||
* Uses the original method if available, otherwise uses items, keys, values.
|
||||
* types:
|
||||
* text_type: unicode in Python 2, str in Python 3
|
||||
* binary_type: str in Python 2, bytes in Python 3
|
||||
* string_types: basestring in Python 2, str in Python 3
|
||||
* bind_method: binds functions to classes
|
||||
* add_metaclass(metaclass) - class decorator that recreates class with with the
|
||||
given metaclass instead (and avoids intermediary class creation)
|
||||
|
||||
Other items:
|
||||
* platform checker
|
||||
"""
|
||||
# pylint disable=W0611
|
||||
# flake8: noqa
|
||||
|
||||
import re
|
||||
import functools
|
||||
import itertools
|
||||
from distutils.version import LooseVersion
|
||||
from itertools import product
|
||||
import sys
|
||||
import platform
|
||||
import types
|
||||
from unicodedata import east_asian_width
|
||||
import struct
|
||||
import inspect
|
||||
from collections import namedtuple
|
||||
|
||||
PY2 = sys.version_info[0] == 2
|
||||
PY3 = sys.version_info[0] >= 3
|
||||
PY35 = sys.version_info >= (3, 5)
|
||||
PY36 = sys.version_info >= (3, 6)
|
||||
PY37 = sys.version_info >= (3, 7)
|
||||
PYPY = platform.python_implementation() == 'PyPy'
|
||||
|
||||
try:
|
||||
import __builtin__ as builtins
|
||||
# not writeable when instantiated with string, doesn't handle unicode well
|
||||
from cStringIO import StringIO as cStringIO
|
||||
# always writeable
|
||||
from StringIO import StringIO
|
||||
BytesIO = StringIO
|
||||
import cPickle
|
||||
import httplib
|
||||
except ImportError:
|
||||
import builtins
|
||||
from io import StringIO, BytesIO
|
||||
cStringIO = StringIO
|
||||
import pickle as cPickle
|
||||
import http.client as httplib
|
||||
|
||||
from pandas.compat.chainmap import DeepChainMap
|
||||
|
||||
|
||||
if PY3:
|
||||
def isidentifier(s):
|
||||
return s.isidentifier()
|
||||
|
||||
def str_to_bytes(s, encoding=None):
|
||||
return s.encode(encoding or 'ascii')
|
||||
|
||||
def bytes_to_str(b, encoding=None):
|
||||
return b.decode(encoding or 'utf-8')
|
||||
|
||||
# The signature version below is directly copied from Django,
|
||||
# https://github.com/django/django/pull/4846
|
||||
def signature(f):
|
||||
sig = inspect.signature(f)
|
||||
args = [
|
||||
p.name for p in sig.parameters.values()
|
||||
if p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
|
||||
]
|
||||
varargs = [
|
||||
p.name for p in sig.parameters.values()
|
||||
if p.kind == inspect.Parameter.VAR_POSITIONAL
|
||||
]
|
||||
varargs = varargs[0] if varargs else None
|
||||
keywords = [
|
||||
p.name for p in sig.parameters.values()
|
||||
if p.kind == inspect.Parameter.VAR_KEYWORD
|
||||
]
|
||||
keywords = keywords[0] if keywords else None
|
||||
defaults = [
|
||||
p.default for p in sig.parameters.values()
|
||||
if p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
|
||||
and p.default is not p.empty
|
||||
] or None
|
||||
argspec = namedtuple('Signature', ['args', 'defaults',
|
||||
'varargs', 'keywords'])
|
||||
return argspec(args, defaults, varargs, keywords)
|
||||
|
||||
def get_range_parameters(data):
|
||||
"""Gets the start, stop, and step parameters from a range object"""
|
||||
return data.start, data.stop, data.step
|
||||
|
||||
# have to explicitly put builtins into the namespace
|
||||
range = range
|
||||
map = map
|
||||
zip = zip
|
||||
filter = filter
|
||||
intern = sys.intern
|
||||
reduce = functools.reduce
|
||||
long = int
|
||||
unichr = chr
|
||||
|
||||
# This was introduced in Python 3.3, but we don't support
|
||||
# Python 3.x < 3.5, so checking PY3 is safe.
|
||||
FileNotFoundError = FileNotFoundError
|
||||
|
||||
# list-producing versions of the major Python iterating functions
|
||||
def lrange(*args, **kwargs):
|
||||
return list(range(*args, **kwargs))
|
||||
|
||||
def lzip(*args, **kwargs):
|
||||
return list(zip(*args, **kwargs))
|
||||
|
||||
def lmap(*args, **kwargs):
|
||||
return list(map(*args, **kwargs))
|
||||
|
||||
def lfilter(*args, **kwargs):
|
||||
return list(filter(*args, **kwargs))
|
||||
|
||||
from importlib import reload
|
||||
reload = reload
|
||||
|
||||
else:
|
||||
# Python 2
|
||||
_name_re = re.compile(r"[a-zA-Z_][a-zA-Z0-9_]*$")
|
||||
|
||||
FileNotFoundError = IOError
|
||||
|
||||
def isidentifier(s, dotted=False):
|
||||
return bool(_name_re.match(s))
|
||||
|
||||
def str_to_bytes(s, encoding='ascii'):
|
||||
return s
|
||||
|
||||
def bytes_to_str(b, encoding='ascii'):
|
||||
return b
|
||||
|
||||
def signature(f):
|
||||
return inspect.getargspec(f)
|
||||
|
||||
def get_range_parameters(data):
|
||||
"""Gets the start, stop, and step parameters from a range object"""
|
||||
# seems we only have indexing ops to infer
|
||||
# rather than direct accessors
|
||||
if len(data) > 1:
|
||||
step = data[1] - data[0]
|
||||
stop = data[-1] + step
|
||||
start = data[0]
|
||||
elif len(data):
|
||||
start = data[0]
|
||||
stop = data[0] + 1
|
||||
step = 1
|
||||
else:
|
||||
start = stop = 0
|
||||
step = 1
|
||||
|
||||
return start, stop, step
|
||||
|
||||
# import iterator versions of these functions
|
||||
range = xrange
|
||||
intern = intern
|
||||
zip = itertools.izip
|
||||
filter = itertools.ifilter
|
||||
map = itertools.imap
|
||||
reduce = reduce
|
||||
long = long
|
||||
unichr = unichr
|
||||
|
||||
# Python 2-builtin ranges produce lists
|
||||
lrange = builtins.range
|
||||
lzip = builtins.zip
|
||||
lmap = builtins.map
|
||||
lfilter = builtins.filter
|
||||
|
||||
reload = builtins.reload
|
||||
|
||||
if PY2:
|
||||
def iteritems(obj, **kw):
|
||||
return obj.iteritems(**kw)
|
||||
|
||||
def iterkeys(obj, **kw):
|
||||
return obj.iterkeys(**kw)
|
||||
|
||||
def itervalues(obj, **kw):
|
||||
return obj.itervalues(**kw)
|
||||
|
||||
next = lambda it: it.next()
|
||||
else:
|
||||
def iteritems(obj, **kw):
|
||||
return iter(obj.items(**kw))
|
||||
|
||||
def iterkeys(obj, **kw):
|
||||
return iter(obj.keys(**kw))
|
||||
|
||||
def itervalues(obj, **kw):
|
||||
return iter(obj.values(**kw))
|
||||
|
||||
next = next
|
||||
|
||||
|
||||
def bind_method(cls, name, func):
|
||||
"""Bind a method to class, python 2 and python 3 compatible.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
cls : type
|
||||
class to receive bound method
|
||||
name : basestring
|
||||
name of method on class instance
|
||||
func : function
|
||||
function to be bound as method
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
# only python 2 has bound/unbound method issue
|
||||
if not PY3:
|
||||
setattr(cls, name, types.MethodType(func, None, cls))
|
||||
else:
|
||||
setattr(cls, name, func)
|
||||
# ----------------------------------------------------------------------------
|
||||
# functions largely based / taken from the six module
|
||||
|
||||
# Much of the code in this module comes from Benjamin Peterson's six library.
|
||||
# The license for this library can be found in LICENSES/SIX and the code can be
|
||||
# found at https://bitbucket.org/gutworth/six
|
||||
|
||||
# Definition of East Asian Width
|
||||
# http://unicode.org/reports/tr11/
|
||||
# Ambiguous width can be changed by option
|
||||
_EAW_MAP = {'Na': 1, 'N': 1, 'W': 2, 'F': 2, 'H': 1}
|
||||
|
||||
if PY3:
|
||||
string_types = str,
|
||||
integer_types = int,
|
||||
class_types = type,
|
||||
text_type = str
|
||||
binary_type = bytes
|
||||
|
||||
def u(s):
|
||||
return s
|
||||
|
||||
def u_safe(s):
|
||||
return s
|
||||
|
||||
def to_str(s):
|
||||
"""
|
||||
Convert bytes and non-string into Python 3 str
|
||||
"""
|
||||
if isinstance(s, binary_type):
|
||||
s = bytes_to_str(s)
|
||||
elif not isinstance(s, string_types):
|
||||
s = str(s)
|
||||
return s
|
||||
|
||||
def strlen(data, encoding=None):
|
||||
# encoding is for compat with PY2
|
||||
return len(data)
|
||||
|
||||
def east_asian_len(data, encoding=None, ambiguous_width=1):
|
||||
"""
|
||||
Calculate display width considering unicode East Asian Width
|
||||
"""
|
||||
if isinstance(data, text_type):
|
||||
return sum(_EAW_MAP.get(east_asian_width(c), ambiguous_width) for c in data)
|
||||
else:
|
||||
return len(data)
|
||||
|
||||
def import_lzma():
|
||||
""" import lzma from the std library """
|
||||
import lzma
|
||||
return lzma
|
||||
|
||||
def set_function_name(f, name, cls):
|
||||
""" Bind the name/qualname attributes of the function """
|
||||
f.__name__ = name
|
||||
f.__qualname__ = '{klass}.{name}'.format(
|
||||
klass=cls.__name__,
|
||||
name=name)
|
||||
f.__module__ = cls.__module__
|
||||
return f
|
||||
|
||||
ResourceWarning = ResourceWarning
|
||||
|
||||
else:
|
||||
string_types = basestring,
|
||||
integer_types = (int, long)
|
||||
class_types = (type, types.ClassType)
|
||||
text_type = unicode
|
||||
binary_type = str
|
||||
|
||||
def u(s):
|
||||
return unicode(s, "unicode_escape")
|
||||
|
||||
def u_safe(s):
|
||||
try:
|
||||
return unicode(s, "unicode_escape")
|
||||
except:
|
||||
return s
|
||||
|
||||
def to_str(s):
|
||||
"""
|
||||
Convert unicode and non-string into Python 2 str
|
||||
"""
|
||||
if not isinstance(s, string_types):
|
||||
s = str(s)
|
||||
return s
|
||||
|
||||
def strlen(data, encoding=None):
|
||||
try:
|
||||
data = data.decode(encoding)
|
||||
except UnicodeError:
|
||||
pass
|
||||
return len(data)
|
||||
|
||||
def east_asian_len(data, encoding=None, ambiguous_width=1):
|
||||
"""
|
||||
Calculate display width considering unicode East Asian Width
|
||||
"""
|
||||
if isinstance(data, text_type):
|
||||
try:
|
||||
data = data.decode(encoding)
|
||||
except UnicodeError:
|
||||
pass
|
||||
return sum(_EAW_MAP.get(east_asian_width(c), ambiguous_width) for c in data)
|
||||
else:
|
||||
return len(data)
|
||||
|
||||
def import_lzma():
|
||||
""" import the backported lzma library
|
||||
or raise ImportError if not available """
|
||||
from backports import lzma
|
||||
return lzma
|
||||
|
||||
def set_function_name(f, name, cls):
|
||||
""" Bind the name attributes of the function """
|
||||
f.__name__ = name
|
||||
return f
|
||||
|
||||
class ResourceWarning(Warning):
|
||||
pass
|
||||
|
||||
string_and_binary_types = string_types + (binary_type,)
|
||||
|
||||
|
||||
try:
|
||||
# callable reintroduced in later versions of Python
|
||||
callable = callable
|
||||
except NameError:
|
||||
def callable(obj):
|
||||
return any("__call__" in klass.__dict__ for klass in type(obj).__mro__)
|
||||
|
||||
|
||||
if PY2:
|
||||
# In PY2 functools.wraps doesn't provide metadata pytest needs to generate
|
||||
# decorated tests using parametrization. See pytest GH issue #2782
|
||||
def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS,
|
||||
updated=functools.WRAPPER_UPDATES):
|
||||
def wrapper(f):
|
||||
f = functools.wraps(wrapped, assigned, updated)(f)
|
||||
f.__wrapped__ = wrapped
|
||||
return f
|
||||
return wrapper
|
||||
else:
|
||||
wraps = functools.wraps
|
||||
|
||||
|
||||
def add_metaclass(metaclass):
|
||||
"""Class decorator for creating a class with a metaclass."""
|
||||
def wrapper(cls):
|
||||
orig_vars = cls.__dict__.copy()
|
||||
orig_vars.pop('__dict__', None)
|
||||
orig_vars.pop('__weakref__', None)
|
||||
for slots_var in orig_vars.get('__slots__', ()):
|
||||
orig_vars.pop(slots_var)
|
||||
return metaclass(cls.__name__, cls.__bases__, orig_vars)
|
||||
return wrapper
|
||||
|
||||
from collections import OrderedDict, Counter
|
||||
|
||||
if PY3:
|
||||
def raise_with_traceback(exc, traceback=Ellipsis):
|
||||
if traceback == Ellipsis:
|
||||
_, _, traceback = sys.exc_info()
|
||||
raise exc.with_traceback(traceback)
|
||||
else:
|
||||
# this version of raise is a syntax error in Python 3
|
||||
exec("""
|
||||
def raise_with_traceback(exc, traceback=Ellipsis):
|
||||
if traceback == Ellipsis:
|
||||
_, _, traceback = sys.exc_info()
|
||||
raise exc, None, traceback
|
||||
""")
|
||||
|
||||
raise_with_traceback.__doc__ = """Raise exception with existing traceback.
|
||||
If traceback is not passed, uses sys.exc_info() to get traceback."""
|
||||
|
||||
|
||||
# dateutil minimum version
|
||||
import dateutil
|
||||
|
||||
if LooseVersion(dateutil.__version__) < LooseVersion('2.5'):
|
||||
raise ImportError('dateutil 2.5.0 is the minimum required version')
|
||||
from dateutil import parser as _date_parser
|
||||
parse_date = _date_parser.parse
|
||||
|
||||
|
||||
# In Python 3.7, the private re._pattern_type is removed.
|
||||
# Python 3.5+ have typing.re.Pattern
|
||||
if PY36:
|
||||
import typing
|
||||
re_type = typing.re.Pattern
|
||||
else:
|
||||
re_type = type(re.compile(''))
|
||||
|
||||
# https://github.com/pandas-dev/pandas/pull/9123
|
||||
def is_platform_little_endian():
|
||||
""" am I little endian """
|
||||
return sys.byteorder == 'little'
|
||||
|
||||
|
||||
def is_platform_windows():
|
||||
return sys.platform == 'win32' or sys.platform == 'cygwin'
|
||||
|
||||
|
||||
def is_platform_linux():
|
||||
return sys.platform == 'linux2'
|
||||
|
||||
|
||||
def is_platform_mac():
|
||||
return sys.platform == 'darwin'
|
||||
|
||||
|
||||
def is_platform_32bit():
|
||||
return struct.calcsize("P") * 8 < 64
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,27 @@
|
||||
try:
|
||||
from collections import ChainMap
|
||||
except ImportError:
|
||||
from pandas.compat.chainmap_impl import ChainMap
|
||||
|
||||
|
||||
class DeepChainMap(ChainMap):
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
for mapping in self.maps:
|
||||
if key in mapping:
|
||||
mapping[key] = value
|
||||
return
|
||||
self.maps[0][key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
for mapping in self.maps:
|
||||
if key in mapping:
|
||||
del mapping[key]
|
||||
return
|
||||
raise KeyError(key)
|
||||
|
||||
# override because the m parameter is introduced in Python 3.4
|
||||
def new_child(self, m=None):
|
||||
if m is None:
|
||||
m = {}
|
||||
return self.__class__(m, *self.maps)
|
||||
@@ -0,0 +1,150 @@
|
||||
from collections import MutableMapping
|
||||
|
||||
try:
|
||||
from thread import get_ident
|
||||
except ImportError:
|
||||
from _thread import get_ident
|
||||
|
||||
|
||||
def recursive_repr(fillvalue='...'):
|
||||
'Decorator to make a repr function return fillvalue for a recursive call'
|
||||
|
||||
def decorating_function(user_function):
|
||||
repr_running = set()
|
||||
|
||||
def wrapper(self):
|
||||
key = id(self), get_ident()
|
||||
if key in repr_running:
|
||||
return fillvalue
|
||||
repr_running.add(key)
|
||||
try:
|
||||
result = user_function(self)
|
||||
finally:
|
||||
repr_running.discard(key)
|
||||
return result
|
||||
|
||||
# Can't use functools.wraps() here because of bootstrap issues
|
||||
wrapper.__module__ = getattr(user_function, '__module__')
|
||||
wrapper.__doc__ = getattr(user_function, '__doc__')
|
||||
wrapper.__name__ = getattr(user_function, '__name__')
|
||||
return wrapper
|
||||
|
||||
return decorating_function
|
||||
|
||||
|
||||
class ChainMap(MutableMapping):
|
||||
""" A ChainMap groups multiple dicts (or other mappings) together
|
||||
to create a single, updatable view.
|
||||
|
||||
The underlying mappings are stored in a list. That list is public and can
|
||||
be accessed / updated using the *maps* attribute. There is no other state.
|
||||
|
||||
Lookups search the underlying mappings successively until a key is found.
|
||||
In contrast, writes, updates, and deletions only operate on the first
|
||||
mapping.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *maps):
|
||||
"""Initialize a ChainMap by setting *maps* to the given mappings.
|
||||
If no mappings are provided, a single empty dictionary is used.
|
||||
|
||||
"""
|
||||
self.maps = list(maps) or [{}] # always at least one map
|
||||
|
||||
def __missing__(self, key):
|
||||
raise KeyError(key)
|
||||
|
||||
def __getitem__(self, key):
|
||||
for mapping in self.maps:
|
||||
try:
|
||||
# can't use 'key in mapping' with defaultdict
|
||||
return mapping[key]
|
||||
except KeyError:
|
||||
pass
|
||||
# support subclasses that define __missing__
|
||||
return self.__missing__(key)
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self[key] if key in self else default
|
||||
|
||||
def __len__(self):
|
||||
# reuses stored hash values if possible
|
||||
return len(set().union(*self.maps))
|
||||
|
||||
def __iter__(self):
|
||||
return iter(set().union(*self.maps))
|
||||
|
||||
def __contains__(self, key):
|
||||
return any(key in m for m in self.maps)
|
||||
|
||||
def __bool__(self):
|
||||
return any(self.maps)
|
||||
|
||||
@recursive_repr()
|
||||
def __repr__(self):
|
||||
return '{0.__class__.__name__}({1})'.format(
|
||||
self, ', '.join(repr(m) for m in self.maps))
|
||||
|
||||
@classmethod
|
||||
def fromkeys(cls, iterable, *args):
|
||||
'Create a ChainMap with a single dict created from the iterable.'
|
||||
return cls(dict.fromkeys(iterable, *args))
|
||||
|
||||
def copy(self):
|
||||
"""
|
||||
New ChainMap or subclass with a new copy of maps[0] and refs to
|
||||
maps[1:]
|
||||
"""
|
||||
return self.__class__(self.maps[0].copy(), *self.maps[1:])
|
||||
|
||||
__copy__ = copy
|
||||
|
||||
def new_child(self, m=None): # like Django's Context.push()
|
||||
"""
|
||||
New ChainMap with a new map followed by all previous maps. If no
|
||||
map is provided, an empty dict is used.
|
||||
"""
|
||||
if m is None:
|
||||
m = {}
|
||||
return self.__class__(m, *self.maps)
|
||||
|
||||
@property
|
||||
def parents(self): # like Django's Context.pop()
|
||||
'New ChainMap from maps[1:].'
|
||||
return self.__class__(*self.maps[1:])
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.maps[0][key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
try:
|
||||
del self.maps[0][key]
|
||||
except KeyError:
|
||||
raise KeyError('Key not found in the first mapping: {!r}'
|
||||
.format(key))
|
||||
|
||||
def popitem(self):
|
||||
"""
|
||||
Remove and return an item pair from maps[0]. Raise KeyError is maps[0]
|
||||
is empty.
|
||||
"""
|
||||
try:
|
||||
return self.maps[0].popitem()
|
||||
except KeyError:
|
||||
raise KeyError('No keys found in the first mapping.')
|
||||
|
||||
def pop(self, key, *args):
|
||||
"""
|
||||
Remove *key* from maps[0] and return its value. Raise KeyError if
|
||||
*key* not in maps[0].
|
||||
"""
|
||||
try:
|
||||
return self.maps[0].pop(key, *args)
|
||||
except KeyError:
|
||||
raise KeyError('Key not found in the first mapping: {!r}'
|
||||
.format(key))
|
||||
|
||||
def clear(self):
|
||||
'Clear maps[0], leaving maps[1:] intact.'
|
||||
self.maps[0].clear()
|
||||
@@ -0,0 +1,78 @@
|
||||
""" support numpy compatiblitiy across versions """
|
||||
|
||||
import re
|
||||
import numpy as np
|
||||
from distutils.version import LooseVersion
|
||||
from pandas.compat import string_types, string_and_binary_types
|
||||
|
||||
|
||||
# numpy versioning
|
||||
_np_version = np.__version__
|
||||
_nlv = LooseVersion(_np_version)
|
||||
_np_version_under1p10 = _nlv < LooseVersion('1.10')
|
||||
_np_version_under1p11 = _nlv < LooseVersion('1.11')
|
||||
_np_version_under1p12 = _nlv < LooseVersion('1.12')
|
||||
_np_version_under1p13 = _nlv < LooseVersion('1.13')
|
||||
_np_version_under1p14 = _nlv < LooseVersion('1.14')
|
||||
_np_version_under1p15 = _nlv < LooseVersion('1.15')
|
||||
|
||||
if _nlv < '1.9':
|
||||
raise ImportError('this version of pandas is incompatible with '
|
||||
'numpy < 1.9.0\n'
|
||||
'your numpy version is {0}.\n'
|
||||
'Please upgrade numpy to >= 1.9.0 to use '
|
||||
'this pandas version'.format(_np_version))
|
||||
|
||||
|
||||
_tz_regex = re.compile('[+-]0000$')
|
||||
|
||||
|
||||
def tz_replacer(s):
|
||||
if isinstance(s, string_types):
|
||||
if s.endswith('Z'):
|
||||
s = s[:-1]
|
||||
elif _tz_regex.search(s):
|
||||
s = s[:-5]
|
||||
return s
|
||||
|
||||
|
||||
def np_datetime64_compat(s, *args, **kwargs):
|
||||
"""
|
||||
provide compat for construction of strings to numpy datetime64's with
|
||||
tz-changes in 1.11 that make '2015-01-01 09:00:00Z' show a deprecation
|
||||
warning, when need to pass '2015-01-01 09:00:00'
|
||||
"""
|
||||
|
||||
if not _np_version_under1p11:
|
||||
s = tz_replacer(s)
|
||||
return np.datetime64(s, *args, **kwargs)
|
||||
|
||||
|
||||
def np_array_datetime64_compat(arr, *args, **kwargs):
|
||||
"""
|
||||
provide compat for construction of an array of strings to a
|
||||
np.array(..., dtype=np.datetime64(..))
|
||||
tz-changes in 1.11 that make '2015-01-01 09:00:00Z' show a deprecation
|
||||
warning, when need to pass '2015-01-01 09:00:00'
|
||||
"""
|
||||
|
||||
if not _np_version_under1p11:
|
||||
|
||||
# is_list_like
|
||||
if hasattr(arr, '__iter__') and not \
|
||||
isinstance(arr, string_and_binary_types):
|
||||
arr = [tz_replacer(s) for s in arr]
|
||||
else:
|
||||
arr = tz_replacer(arr)
|
||||
|
||||
return np.array(arr, *args, **kwargs)
|
||||
|
||||
|
||||
__all__ = ['np',
|
||||
'_np_version_under1p10',
|
||||
'_np_version_under1p11',
|
||||
'_np_version_under1p12',
|
||||
'_np_version_under1p13',
|
||||
'_np_version_under1p14',
|
||||
'_np_version_under1p15'
|
||||
]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,362 @@
|
||||
"""
|
||||
For compatibility with numpy libraries, pandas functions or
|
||||
methods have to accept '*args' and '**kwargs' parameters to
|
||||
accommodate numpy arguments that are not actually used or
|
||||
respected in the pandas implementation.
|
||||
|
||||
To ensure that users do not abuse these parameters, validation
|
||||
is performed in 'validators.py' to make sure that any extra
|
||||
parameters passed correspond ONLY to those in the numpy signature.
|
||||
Part of that validation includes whether or not the user attempted
|
||||
to pass in non-default values for these extraneous parameters. As we
|
||||
want to discourage users from relying on these parameters when calling
|
||||
the pandas implementation, we want them only to pass in the default values
|
||||
for these parameters.
|
||||
|
||||
This module provides a set of commonly used default arguments for functions
|
||||
and methods that are spread throughout the codebase. This module will make it
|
||||
easier to adjust to future upstream changes in the analogous numpy signatures.
|
||||
"""
|
||||
|
||||
from numpy import ndarray
|
||||
from pandas.util._validators import (validate_args, validate_kwargs,
|
||||
validate_args_and_kwargs)
|
||||
from pandas.errors import UnsupportedFunctionCall
|
||||
from pandas.core.dtypes.common import is_integer, is_bool
|
||||
from pandas.compat import OrderedDict
|
||||
|
||||
|
||||
class CompatValidator(object):
|
||||
|
||||
def __init__(self, defaults, fname=None, method=None,
|
||||
max_fname_arg_count=None):
|
||||
self.fname = fname
|
||||
self.method = method
|
||||
self.defaults = defaults
|
||||
self.max_fname_arg_count = max_fname_arg_count
|
||||
|
||||
def __call__(self, args, kwargs, fname=None,
|
||||
max_fname_arg_count=None, method=None):
|
||||
if args or kwargs:
|
||||
fname = self.fname if fname is None else fname
|
||||
max_fname_arg_count = (self.max_fname_arg_count if
|
||||
max_fname_arg_count is None
|
||||
else max_fname_arg_count)
|
||||
method = self.method if method is None else method
|
||||
|
||||
if method == 'args':
|
||||
validate_args(fname, args, max_fname_arg_count, self.defaults)
|
||||
elif method == 'kwargs':
|
||||
validate_kwargs(fname, kwargs, self.defaults)
|
||||
elif method == 'both':
|
||||
validate_args_and_kwargs(fname, args, kwargs,
|
||||
max_fname_arg_count,
|
||||
self.defaults)
|
||||
else:
|
||||
raise ValueError("invalid validation method "
|
||||
"'{method}'".format(method=method))
|
||||
|
||||
|
||||
ARGMINMAX_DEFAULTS = dict(out=None)
|
||||
validate_argmin = CompatValidator(ARGMINMAX_DEFAULTS, fname='argmin',
|
||||
method='both', max_fname_arg_count=1)
|
||||
validate_argmax = CompatValidator(ARGMINMAX_DEFAULTS, fname='argmax',
|
||||
method='both', max_fname_arg_count=1)
|
||||
|
||||
|
||||
def process_skipna(skipna, args):
|
||||
if isinstance(skipna, ndarray) or skipna is None:
|
||||
args = (skipna,) + args
|
||||
skipna = True
|
||||
|
||||
return skipna, args
|
||||
|
||||
|
||||
def validate_argmin_with_skipna(skipna, args, kwargs):
|
||||
"""
|
||||
If 'Series.argmin' is called via the 'numpy' library,
|
||||
the third parameter in its signature is 'out', which
|
||||
takes either an ndarray or 'None', so check if the
|
||||
'skipna' parameter is either an instance of ndarray or
|
||||
is None, since 'skipna' itself should be a boolean
|
||||
"""
|
||||
|
||||
skipna, args = process_skipna(skipna, args)
|
||||
validate_argmin(args, kwargs)
|
||||
return skipna
|
||||
|
||||
|
||||
def validate_argmax_with_skipna(skipna, args, kwargs):
|
||||
"""
|
||||
If 'Series.argmax' is called via the 'numpy' library,
|
||||
the third parameter in its signature is 'out', which
|
||||
takes either an ndarray or 'None', so check if the
|
||||
'skipna' parameter is either an instance of ndarray or
|
||||
is None, since 'skipna' itself should be a boolean
|
||||
"""
|
||||
|
||||
skipna, args = process_skipna(skipna, args)
|
||||
validate_argmax(args, kwargs)
|
||||
return skipna
|
||||
|
||||
|
||||
ARGSORT_DEFAULTS = OrderedDict()
|
||||
ARGSORT_DEFAULTS['axis'] = -1
|
||||
ARGSORT_DEFAULTS['kind'] = 'quicksort'
|
||||
ARGSORT_DEFAULTS['order'] = None
|
||||
validate_argsort = CompatValidator(ARGSORT_DEFAULTS, fname='argsort',
|
||||
max_fname_arg_count=0, method='both')
|
||||
|
||||
# two different signatures of argsort, this second validation
|
||||
# for when the `kind` param is supported
|
||||
ARGSORT_DEFAULTS_KIND = OrderedDict()
|
||||
ARGSORT_DEFAULTS_KIND['axis'] = -1
|
||||
ARGSORT_DEFAULTS_KIND['order'] = None
|
||||
validate_argsort_kind = CompatValidator(ARGSORT_DEFAULTS_KIND, fname='argsort',
|
||||
max_fname_arg_count=0, method='both')
|
||||
|
||||
|
||||
def validate_argsort_with_ascending(ascending, args, kwargs):
|
||||
"""
|
||||
If 'Categorical.argsort' is called via the 'numpy' library, the
|
||||
first parameter in its signature is 'axis', which takes either
|
||||
an integer or 'None', so check if the 'ascending' parameter has
|
||||
either integer type or is None, since 'ascending' itself should
|
||||
be a boolean
|
||||
"""
|
||||
|
||||
if is_integer(ascending) or ascending is None:
|
||||
args = (ascending,) + args
|
||||
ascending = True
|
||||
|
||||
validate_argsort_kind(args, kwargs, max_fname_arg_count=3)
|
||||
return ascending
|
||||
|
||||
|
||||
CLIP_DEFAULTS = dict(out=None)
|
||||
validate_clip = CompatValidator(CLIP_DEFAULTS, fname='clip',
|
||||
method='both', max_fname_arg_count=3)
|
||||
|
||||
|
||||
def validate_clip_with_axis(axis, args, kwargs):
|
||||
"""
|
||||
If 'NDFrame.clip' is called via the numpy library, the third
|
||||
parameter in its signature is 'out', which can takes an ndarray,
|
||||
so check if the 'axis' parameter is an instance of ndarray, since
|
||||
'axis' itself should either be an integer or None
|
||||
"""
|
||||
|
||||
if isinstance(axis, ndarray):
|
||||
args = (axis,) + args
|
||||
axis = None
|
||||
|
||||
validate_clip(args, kwargs)
|
||||
return axis
|
||||
|
||||
|
||||
COMPRESS_DEFAULTS = OrderedDict()
|
||||
COMPRESS_DEFAULTS['axis'] = None
|
||||
COMPRESS_DEFAULTS['out'] = None
|
||||
validate_compress = CompatValidator(COMPRESS_DEFAULTS, fname='compress',
|
||||
method='both', max_fname_arg_count=1)
|
||||
|
||||
CUM_FUNC_DEFAULTS = OrderedDict()
|
||||
CUM_FUNC_DEFAULTS['dtype'] = None
|
||||
CUM_FUNC_DEFAULTS['out'] = None
|
||||
validate_cum_func = CompatValidator(CUM_FUNC_DEFAULTS, method='both',
|
||||
max_fname_arg_count=1)
|
||||
validate_cumsum = CompatValidator(CUM_FUNC_DEFAULTS, fname='cumsum',
|
||||
method='both', max_fname_arg_count=1)
|
||||
|
||||
|
||||
def validate_cum_func_with_skipna(skipna, args, kwargs, name):
|
||||
"""
|
||||
If this function is called via the 'numpy' library, the third
|
||||
parameter in its signature is 'dtype', which takes either a
|
||||
'numpy' dtype or 'None', so check if the 'skipna' parameter is
|
||||
a boolean or not
|
||||
"""
|
||||
if not is_bool(skipna):
|
||||
args = (skipna,) + args
|
||||
skipna = True
|
||||
|
||||
validate_cum_func(args, kwargs, fname=name)
|
||||
return skipna
|
||||
|
||||
|
||||
ALLANY_DEFAULTS = OrderedDict()
|
||||
ALLANY_DEFAULTS['dtype'] = None
|
||||
ALLANY_DEFAULTS['out'] = None
|
||||
validate_all = CompatValidator(ALLANY_DEFAULTS, fname='all',
|
||||
method='both', max_fname_arg_count=1)
|
||||
validate_any = CompatValidator(ALLANY_DEFAULTS, fname='any',
|
||||
method='both', max_fname_arg_count=1)
|
||||
|
||||
LOGICAL_FUNC_DEFAULTS = dict(out=None)
|
||||
validate_logical_func = CompatValidator(LOGICAL_FUNC_DEFAULTS, method='kwargs')
|
||||
|
||||
MINMAX_DEFAULTS = dict(out=None)
|
||||
validate_min = CompatValidator(MINMAX_DEFAULTS, fname='min',
|
||||
method='both', max_fname_arg_count=1)
|
||||
validate_max = CompatValidator(MINMAX_DEFAULTS, fname='max',
|
||||
method='both', max_fname_arg_count=1)
|
||||
|
||||
RESHAPE_DEFAULTS = dict(order='C')
|
||||
validate_reshape = CompatValidator(RESHAPE_DEFAULTS, fname='reshape',
|
||||
method='both', max_fname_arg_count=1)
|
||||
|
||||
REPEAT_DEFAULTS = dict(axis=None)
|
||||
validate_repeat = CompatValidator(REPEAT_DEFAULTS, fname='repeat',
|
||||
method='both', max_fname_arg_count=1)
|
||||
|
||||
ROUND_DEFAULTS = dict(out=None)
|
||||
validate_round = CompatValidator(ROUND_DEFAULTS, fname='round',
|
||||
method='both', max_fname_arg_count=1)
|
||||
|
||||
SORT_DEFAULTS = OrderedDict()
|
||||
SORT_DEFAULTS['axis'] = -1
|
||||
SORT_DEFAULTS['kind'] = 'quicksort'
|
||||
SORT_DEFAULTS['order'] = None
|
||||
validate_sort = CompatValidator(SORT_DEFAULTS, fname='sort',
|
||||
method='kwargs')
|
||||
|
||||
STAT_FUNC_DEFAULTS = OrderedDict()
|
||||
STAT_FUNC_DEFAULTS['dtype'] = None
|
||||
STAT_FUNC_DEFAULTS['out'] = None
|
||||
validate_stat_func = CompatValidator(STAT_FUNC_DEFAULTS,
|
||||
method='kwargs')
|
||||
validate_sum = CompatValidator(STAT_FUNC_DEFAULTS, fname='sort',
|
||||
method='both', max_fname_arg_count=1)
|
||||
validate_mean = CompatValidator(STAT_FUNC_DEFAULTS, fname='mean',
|
||||
method='both', max_fname_arg_count=1)
|
||||
|
||||
STAT_DDOF_FUNC_DEFAULTS = OrderedDict()
|
||||
STAT_DDOF_FUNC_DEFAULTS['dtype'] = None
|
||||
STAT_DDOF_FUNC_DEFAULTS['out'] = None
|
||||
validate_stat_ddof_func = CompatValidator(STAT_DDOF_FUNC_DEFAULTS,
|
||||
method='kwargs')
|
||||
|
||||
TAKE_DEFAULTS = OrderedDict()
|
||||
TAKE_DEFAULTS['out'] = None
|
||||
TAKE_DEFAULTS['mode'] = 'raise'
|
||||
validate_take = CompatValidator(TAKE_DEFAULTS, fname='take',
|
||||
method='kwargs')
|
||||
|
||||
|
||||
def validate_take_with_convert(convert, args, kwargs):
|
||||
"""
|
||||
If this function is called via the 'numpy' library, the third
|
||||
parameter in its signature is 'axis', which takes either an
|
||||
ndarray or 'None', so check if the 'convert' parameter is either
|
||||
an instance of ndarray or is None
|
||||
"""
|
||||
|
||||
if isinstance(convert, ndarray) or convert is None:
|
||||
args = (convert,) + args
|
||||
convert = True
|
||||
|
||||
validate_take(args, kwargs, max_fname_arg_count=3, method='both')
|
||||
return convert
|
||||
|
||||
|
||||
TRANSPOSE_DEFAULTS = dict(axes=None)
|
||||
validate_transpose = CompatValidator(TRANSPOSE_DEFAULTS, fname='transpose',
|
||||
method='both', max_fname_arg_count=0)
|
||||
|
||||
|
||||
def validate_transpose_for_generic(inst, kwargs):
|
||||
try:
|
||||
validate_transpose(tuple(), kwargs)
|
||||
except ValueError as e:
|
||||
klass = type(inst).__name__
|
||||
msg = str(e)
|
||||
|
||||
# the Panel class actual relies on the 'axes' parameter if called
|
||||
# via the 'numpy' library, so let's make sure the error is specific
|
||||
# about saying that the parameter is not supported for particular
|
||||
# implementations of 'transpose'
|
||||
if "the 'axes' parameter is not supported" in msg:
|
||||
msg += " for {klass} instances".format(klass=klass)
|
||||
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def validate_window_func(name, args, kwargs):
|
||||
numpy_args = ('axis', 'dtype', 'out')
|
||||
msg = ("numpy operations are not "
|
||||
"valid with window objects. "
|
||||
"Use .{func}() directly instead ".format(func=name))
|
||||
|
||||
if len(args) > 0:
|
||||
raise UnsupportedFunctionCall(msg)
|
||||
|
||||
for arg in numpy_args:
|
||||
if arg in kwargs:
|
||||
raise UnsupportedFunctionCall(msg)
|
||||
|
||||
|
||||
def validate_rolling_func(name, args, kwargs):
|
||||
numpy_args = ('axis', 'dtype', 'out')
|
||||
msg = ("numpy operations are not "
|
||||
"valid with window objects. "
|
||||
"Use .rolling(...).{func}() instead ".format(func=name))
|
||||
|
||||
if len(args) > 0:
|
||||
raise UnsupportedFunctionCall(msg)
|
||||
|
||||
for arg in numpy_args:
|
||||
if arg in kwargs:
|
||||
raise UnsupportedFunctionCall(msg)
|
||||
|
||||
|
||||
def validate_expanding_func(name, args, kwargs):
|
||||
numpy_args = ('axis', 'dtype', 'out')
|
||||
msg = ("numpy operations are not "
|
||||
"valid with window objects. "
|
||||
"Use .expanding(...).{func}() instead ".format(func=name))
|
||||
|
||||
if len(args) > 0:
|
||||
raise UnsupportedFunctionCall(msg)
|
||||
|
||||
for arg in numpy_args:
|
||||
if arg in kwargs:
|
||||
raise UnsupportedFunctionCall(msg)
|
||||
|
||||
|
||||
def validate_groupby_func(name, args, kwargs, allowed=None):
|
||||
"""
|
||||
'args' and 'kwargs' should be empty, except for allowed
|
||||
kwargs because all of
|
||||
their necessary parameters are explicitly listed in
|
||||
the function signature
|
||||
"""
|
||||
if allowed is None:
|
||||
allowed = []
|
||||
|
||||
kwargs = set(kwargs) - set(allowed)
|
||||
|
||||
if len(args) + len(kwargs) > 0:
|
||||
raise UnsupportedFunctionCall((
|
||||
"numpy operations are not valid "
|
||||
"with groupby. Use .groupby(...)."
|
||||
"{func}() instead".format(func=name)))
|
||||
|
||||
|
||||
RESAMPLER_NUMPY_OPS = ('min', 'max', 'sum', 'prod',
|
||||
'mean', 'std', 'var')
|
||||
|
||||
|
||||
def validate_resampler_func(method, args, kwargs):
|
||||
"""
|
||||
'args' and 'kwargs' should be empty because all of
|
||||
their necessary parameters are explicitly listed in
|
||||
the function signature
|
||||
"""
|
||||
if len(args) + len(kwargs) > 0:
|
||||
if method in RESAMPLER_NUMPY_OPS:
|
||||
raise UnsupportedFunctionCall((
|
||||
"numpy operations are not valid "
|
||||
"with resample. Use .resample(...)."
|
||||
"{func}() instead".format(func=method)))
|
||||
else:
|
||||
raise TypeError("too many arguments passed in")
|
||||
@@ -0,0 +1,214 @@
|
||||
"""
|
||||
Support pre-0.12 series pickle compatibility.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import pandas # noqa
|
||||
import copy
|
||||
import pickle as pkl
|
||||
from pandas import compat, Index
|
||||
from pandas.compat import u, string_types # noqa
|
||||
|
||||
|
||||
def load_reduce(self):
|
||||
stack = self.stack
|
||||
args = stack.pop()
|
||||
func = stack[-1]
|
||||
|
||||
if len(args) and type(args[0]) is type:
|
||||
n = args[0].__name__ # noqa
|
||||
|
||||
try:
|
||||
stack[-1] = func(*args)
|
||||
return
|
||||
except Exception as e:
|
||||
|
||||
# If we have a deprecated function,
|
||||
# try to replace and try again.
|
||||
|
||||
msg = '_reconstruct: First argument must be a sub-type of ndarray'
|
||||
|
||||
if msg in str(e):
|
||||
try:
|
||||
cls = args[0]
|
||||
stack[-1] = object.__new__(cls)
|
||||
return
|
||||
except:
|
||||
pass
|
||||
|
||||
# try to re-encode the arguments
|
||||
if getattr(self, 'encoding', None) is not None:
|
||||
args = tuple(arg.encode(self.encoding)
|
||||
if isinstance(arg, string_types)
|
||||
else arg for arg in args)
|
||||
try:
|
||||
stack[-1] = func(*args)
|
||||
return
|
||||
except:
|
||||
pass
|
||||
|
||||
# unknown exception, re-raise
|
||||
if getattr(self, 'is_verbose', None):
|
||||
print(sys.exc_info())
|
||||
print(func, args)
|
||||
raise
|
||||
|
||||
|
||||
# If classes are moved, provide compat here.
|
||||
_class_locations_map = {
|
||||
|
||||
# 15477
|
||||
('pandas.core.base', 'FrozenNDArray'):
|
||||
('pandas.core.indexes.frozen', 'FrozenNDArray'),
|
||||
('pandas.core.base', 'FrozenList'):
|
||||
('pandas.core.indexes.frozen', 'FrozenList'),
|
||||
|
||||
# 10890
|
||||
('pandas.core.series', 'TimeSeries'):
|
||||
('pandas.core.series', 'Series'),
|
||||
('pandas.sparse.series', 'SparseTimeSeries'):
|
||||
('pandas.core.sparse.series', 'SparseSeries'),
|
||||
|
||||
# 12588, extensions moving
|
||||
('pandas._sparse', 'BlockIndex'):
|
||||
('pandas._libs.sparse', 'BlockIndex'),
|
||||
('pandas.tslib', 'Timestamp'):
|
||||
('pandas._libs.tslib', 'Timestamp'),
|
||||
|
||||
# 18543 moving period
|
||||
('pandas._period', 'Period'): ('pandas._libs.tslibs.period', 'Period'),
|
||||
('pandas._libs.period', 'Period'):
|
||||
('pandas._libs.tslibs.period', 'Period'),
|
||||
|
||||
# 18014 moved __nat_unpickle from _libs.tslib-->_libs.tslibs.nattype
|
||||
('pandas.tslib', '__nat_unpickle'):
|
||||
('pandas._libs.tslibs.nattype', '__nat_unpickle'),
|
||||
('pandas._libs.tslib', '__nat_unpickle'):
|
||||
('pandas._libs.tslibs.nattype', '__nat_unpickle'),
|
||||
|
||||
# 15998 top-level dirs moving
|
||||
('pandas.sparse.array', 'SparseArray'):
|
||||
('pandas.core.sparse.array', 'SparseArray'),
|
||||
('pandas.sparse.series', 'SparseSeries'):
|
||||
('pandas.core.sparse.series', 'SparseSeries'),
|
||||
('pandas.sparse.frame', 'SparseDataFrame'):
|
||||
('pandas.core.sparse.frame', 'SparseDataFrame'),
|
||||
('pandas.indexes.base', '_new_Index'):
|
||||
('pandas.core.indexes.base', '_new_Index'),
|
||||
('pandas.indexes.base', 'Index'):
|
||||
('pandas.core.indexes.base', 'Index'),
|
||||
('pandas.indexes.numeric', 'Int64Index'):
|
||||
('pandas.core.indexes.numeric', 'Int64Index'),
|
||||
('pandas.indexes.range', 'RangeIndex'):
|
||||
('pandas.core.indexes.range', 'RangeIndex'),
|
||||
('pandas.indexes.multi', 'MultiIndex'):
|
||||
('pandas.core.indexes.multi', 'MultiIndex'),
|
||||
('pandas.tseries.index', '_new_DatetimeIndex'):
|
||||
('pandas.core.indexes.datetimes', '_new_DatetimeIndex'),
|
||||
('pandas.tseries.index', 'DatetimeIndex'):
|
||||
('pandas.core.indexes.datetimes', 'DatetimeIndex'),
|
||||
('pandas.tseries.period', 'PeriodIndex'):
|
||||
('pandas.core.indexes.period', 'PeriodIndex'),
|
||||
|
||||
# 19269, arrays moving
|
||||
('pandas.core.categorical', 'Categorical'):
|
||||
('pandas.core.arrays', 'Categorical'),
|
||||
|
||||
# 19939, add timedeltaindex, float64index compat from 15998 move
|
||||
('pandas.tseries.tdi', 'TimedeltaIndex'):
|
||||
('pandas.core.indexes.timedeltas', 'TimedeltaIndex'),
|
||||
('pandas.indexes.numeric', 'Float64Index'):
|
||||
('pandas.core.indexes.numeric', 'Float64Index'),
|
||||
}
|
||||
|
||||
|
||||
# our Unpickler sub-class to override methods and some dispatcher
|
||||
# functions for compat
|
||||
|
||||
if compat.PY3:
|
||||
class Unpickler(pkl._Unpickler):
|
||||
|
||||
def find_class(self, module, name):
|
||||
# override superclass
|
||||
key = (module, name)
|
||||
module, name = _class_locations_map.get(key, key)
|
||||
return super(Unpickler, self).find_class(module, name)
|
||||
|
||||
else:
|
||||
|
||||
class Unpickler(pkl.Unpickler):
|
||||
|
||||
def find_class(self, module, name):
|
||||
# override superclass
|
||||
key = (module, name)
|
||||
module, name = _class_locations_map.get(key, key)
|
||||
__import__(module)
|
||||
mod = sys.modules[module]
|
||||
klass = getattr(mod, name)
|
||||
return klass
|
||||
|
||||
Unpickler.dispatch = copy.copy(Unpickler.dispatch)
|
||||
Unpickler.dispatch[pkl.REDUCE[0]] = load_reduce
|
||||
|
||||
|
||||
def load_newobj(self):
|
||||
args = self.stack.pop()
|
||||
cls = self.stack[-1]
|
||||
|
||||
# compat
|
||||
if issubclass(cls, Index):
|
||||
obj = object.__new__(cls)
|
||||
else:
|
||||
obj = cls.__new__(cls, *args)
|
||||
|
||||
self.stack[-1] = obj
|
||||
|
||||
|
||||
Unpickler.dispatch[pkl.NEWOBJ[0]] = load_newobj
|
||||
|
||||
|
||||
def load_newobj_ex(self):
|
||||
kwargs = self.stack.pop()
|
||||
args = self.stack.pop()
|
||||
cls = self.stack.pop()
|
||||
|
||||
# compat
|
||||
if issubclass(cls, Index):
|
||||
obj = object.__new__(cls)
|
||||
else:
|
||||
obj = cls.__new__(cls, *args, **kwargs)
|
||||
self.append(obj)
|
||||
|
||||
|
||||
try:
|
||||
Unpickler.dispatch[pkl.NEWOBJ_EX[0]] = load_newobj_ex
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def load(fh, encoding=None, compat=False, is_verbose=False):
|
||||
"""load a pickle, with a provided encoding
|
||||
|
||||
if compat is True:
|
||||
fake the old class hierarchy
|
||||
if it works, then return the new type objects
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fh: a filelike object
|
||||
encoding: an optional encoding
|
||||
compat: provide Series compatibility mode, boolean, default False
|
||||
is_verbose: show exception output
|
||||
"""
|
||||
|
||||
try:
|
||||
fh.seek(0)
|
||||
if encoding is not None:
|
||||
up = Unpickler(fh, encoding=encoding)
|
||||
else:
|
||||
up = Unpickler(fh)
|
||||
up.is_verbose = is_verbose
|
||||
|
||||
return up.load()
|
||||
except:
|
||||
raise
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,15 @@
|
||||
import warnings
|
||||
|
||||
|
||||
def set_use_numexpr(v=True):
|
||||
"""
|
||||
.. deprecated:: 0.20.0
|
||||
Use ``pandas.set_option('compute.use_numexpr', v)`` instead.
|
||||
"""
|
||||
warnings.warn("pandas.computation.expressions.set_use_numexpr is "
|
||||
"deprecated and will be removed in a future version.\n"
|
||||
"you can toggle usage of numexpr via "
|
||||
"pandas.get_option('compute.use_numexpr')",
|
||||
FutureWarning, stacklevel=2)
|
||||
from pandas import set_option
|
||||
set_option('compute.use_numexpr', v)
|
||||
@@ -0,0 +1,285 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
import pandas
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from pandas.compat import PY3
|
||||
import pandas.util._test_decorators as td
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("--skip-slow", action="store_true",
|
||||
help="skip slow tests")
|
||||
parser.addoption("--skip-network", action="store_true",
|
||||
help="skip network tests")
|
||||
parser.addoption("--run-high-memory", action="store_true",
|
||||
help="run high memory tests")
|
||||
parser.addoption("--only-slow", action="store_true",
|
||||
help="run only slow tests")
|
||||
parser.addoption("--strict-data-files", action="store_true",
|
||||
help="Fail if a test is skipped for missing data file.")
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
if 'slow' in item.keywords and item.config.getoption("--skip-slow"):
|
||||
pytest.skip("skipping due to --skip-slow")
|
||||
|
||||
if 'slow' not in item.keywords and item.config.getoption("--only-slow"):
|
||||
pytest.skip("skipping due to --only-slow")
|
||||
|
||||
if 'network' in item.keywords and item.config.getoption("--skip-network"):
|
||||
pytest.skip("skipping due to --skip-network")
|
||||
|
||||
if 'high_memory' in item.keywords and not item.config.getoption(
|
||||
"--run-high-memory"):
|
||||
pytest.skip(
|
||||
"skipping high memory test since --run-high-memory was not set")
|
||||
|
||||
|
||||
# Configurations for all tests and all test modules
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def configure_tests():
|
||||
pd.set_option('chained_assignment', 'raise')
|
||||
|
||||
|
||||
# For running doctests: make np and pd names available
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def add_imports(doctest_namespace):
|
||||
doctest_namespace['np'] = np
|
||||
doctest_namespace['pd'] = pd
|
||||
|
||||
|
||||
@pytest.fixture(params=['bsr', 'coo', 'csc', 'csr', 'dia', 'dok', 'lil'])
|
||||
def spmatrix(request):
|
||||
from scipy import sparse
|
||||
return getattr(sparse, request.param + '_matrix')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ip():
|
||||
"""
|
||||
Get an instance of IPython.InteractiveShell.
|
||||
|
||||
Will raise a skip if IPython is not installed.
|
||||
"""
|
||||
|
||||
pytest.importorskip('IPython', minversion="6.0.0")
|
||||
from IPython.core.interactiveshell import InteractiveShell
|
||||
return InteractiveShell()
|
||||
|
||||
|
||||
@pytest.fixture(params=[True, False, None])
|
||||
def observed(request):
|
||||
""" pass in the observed keyword to groupby for [True, False]
|
||||
This indicates whether categoricals should return values for
|
||||
values which are not in the grouper [False / None], or only values which
|
||||
appear in the grouper [True]. [None] is supported for future compatiblity
|
||||
if we decide to change the default (and would need to warn if this
|
||||
parameter is not passed)"""
|
||||
return request.param
|
||||
|
||||
|
||||
_all_arithmetic_operators = ['__add__', '__radd__',
|
||||
'__sub__', '__rsub__',
|
||||
'__mul__', '__rmul__',
|
||||
'__floordiv__', '__rfloordiv__',
|
||||
'__truediv__', '__rtruediv__',
|
||||
'__pow__', '__rpow__']
|
||||
if not PY3:
|
||||
_all_arithmetic_operators.extend(['__div__', '__rdiv__'])
|
||||
|
||||
|
||||
@pytest.fixture(params=_all_arithmetic_operators)
|
||||
def all_arithmetic_operators(request):
|
||||
"""
|
||||
Fixture for dunder names for common arithmetic operations
|
||||
"""
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(params=[None, 'gzip', 'bz2', 'zip',
|
||||
pytest.param('xz', marks=td.skip_if_no_lzma)])
|
||||
def compression(request):
|
||||
"""
|
||||
Fixture for trying common compression types in compression tests
|
||||
"""
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(params=['gzip', 'bz2', 'zip',
|
||||
pytest.param('xz', marks=td.skip_if_no_lzma)])
|
||||
def compression_only(request):
|
||||
"""
|
||||
Fixture for trying common compression types in compression tests excluding
|
||||
uncompressed case
|
||||
"""
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def datetime_tz_utc():
|
||||
from datetime import timezone
|
||||
return timezone.utc
|
||||
|
||||
|
||||
@pytest.fixture(params=['inner', 'outer', 'left', 'right'])
|
||||
def join_type(request):
|
||||
"""
|
||||
Fixture for trying all types of join operations
|
||||
"""
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def datapath(request):
|
||||
"""Get the path to a data file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : str
|
||||
Path to the file, relative to ``pandas/tests/``
|
||||
|
||||
Returns
|
||||
-------
|
||||
path : path including ``pandas/tests``.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If the path doesn't exist and the --strict-data-files option is set.
|
||||
"""
|
||||
def deco(*args):
|
||||
path = os.path.join('pandas', 'tests', *args)
|
||||
if not os.path.exists(path):
|
||||
if request.config.getoption("--strict-data-files"):
|
||||
msg = "Could not find file {} and --strict-data-files is set."
|
||||
raise ValueError(msg.format(path))
|
||||
else:
|
||||
msg = "Could not find {}."
|
||||
pytest.skip(msg.format(path))
|
||||
return path
|
||||
return deco
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def iris(datapath):
|
||||
"""The iris dataset as a DataFrame."""
|
||||
return pandas.read_csv(datapath('data', 'iris.csv'))
|
||||
|
||||
|
||||
@pytest.fixture(params=['nlargest', 'nsmallest'])
|
||||
def nselect_method(request):
|
||||
"""
|
||||
Fixture for trying all nselect methods
|
||||
"""
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(params=[None, np.nan, pd.NaT, float('nan'), np.float('NaN')])
|
||||
def nulls_fixture(request):
|
||||
"""
|
||||
Fixture for each null type in pandas
|
||||
"""
|
||||
return request.param
|
||||
|
||||
|
||||
nulls_fixture2 = nulls_fixture # Generate cartesian product of nulls_fixture
|
||||
|
||||
|
||||
TIMEZONES = [None, 'UTC', 'US/Eastern', 'Asia/Tokyo', 'dateutil/US/Pacific']
|
||||
|
||||
|
||||
@td.parametrize_fixture_doc(str(TIMEZONES))
|
||||
@pytest.fixture(params=TIMEZONES)
|
||||
def tz_naive_fixture(request):
|
||||
"""
|
||||
Fixture for trying timezones including default (None): {0}
|
||||
"""
|
||||
return request.param
|
||||
|
||||
|
||||
@td.parametrize_fixture_doc(str(TIMEZONES[1:]))
|
||||
@pytest.fixture(params=TIMEZONES[1:])
|
||||
def tz_aware_fixture(request):
|
||||
"""
|
||||
Fixture for trying explicit timezones: {0}
|
||||
"""
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(params=[str, 'str', 'U'])
|
||||
def string_dtype(request):
|
||||
"""Parametrized fixture for string dtypes.
|
||||
|
||||
* str
|
||||
* 'str'
|
||||
* 'U'
|
||||
"""
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(params=["float32", "float64"])
|
||||
def float_dtype(request):
|
||||
"""
|
||||
Parameterized fixture for float dtypes.
|
||||
|
||||
* float32
|
||||
* float64
|
||||
"""
|
||||
|
||||
return request.param
|
||||
|
||||
|
||||
UNSIGNED_INT_DTYPES = ["uint8", "uint16", "uint32", "uint64"]
|
||||
SIGNED_INT_DTYPES = ["int8", "int16", "int32", "int64"]
|
||||
ALL_INT_DTYPES = UNSIGNED_INT_DTYPES + SIGNED_INT_DTYPES
|
||||
|
||||
|
||||
@pytest.fixture(params=SIGNED_INT_DTYPES)
|
||||
def sint_dtype(request):
|
||||
"""
|
||||
Parameterized fixture for signed integer dtypes.
|
||||
|
||||
* int8
|
||||
* int16
|
||||
* int32
|
||||
* int64
|
||||
"""
|
||||
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(params=UNSIGNED_INT_DTYPES)
|
||||
def uint_dtype(request):
|
||||
"""
|
||||
Parameterized fixture for unsigned integer dtypes.
|
||||
|
||||
* uint8
|
||||
* uint16
|
||||
* uint32
|
||||
* uint64
|
||||
"""
|
||||
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(params=ALL_INT_DTYPES)
|
||||
def any_int_dtype(request):
|
||||
"""
|
||||
Parameterized fixture for any integer dtypes.
|
||||
|
||||
* int8
|
||||
* uint8
|
||||
* int16
|
||||
* uint16
|
||||
* int32
|
||||
* uint32
|
||||
* int64
|
||||
* uint64
|
||||
"""
|
||||
|
||||
return request.param
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,241 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
|
||||
accessor.py contains base classes for implementing accessor properties
|
||||
that can be mixed into or pinned onto other pandas classes.
|
||||
|
||||
"""
|
||||
import warnings
|
||||
|
||||
from pandas.util._decorators import Appender
|
||||
|
||||
|
||||
class DirNamesMixin(object):
|
||||
_accessors = frozenset([])
|
||||
_deprecations = frozenset(
|
||||
['asobject', 'base', 'data', 'flags', 'itemsize', 'strides'])
|
||||
|
||||
def _dir_deletions(self):
|
||||
""" delete unwanted __dir__ for this object """
|
||||
return self._accessors | self._deprecations
|
||||
|
||||
def _dir_additions(self):
|
||||
""" add additional __dir__ for this object """
|
||||
rv = set()
|
||||
for accessor in self._accessors:
|
||||
try:
|
||||
getattr(self, accessor)
|
||||
rv.add(accessor)
|
||||
except AttributeError:
|
||||
pass
|
||||
return rv
|
||||
|
||||
def __dir__(self):
|
||||
"""
|
||||
Provide method name lookup and completion
|
||||
Only provide 'public' methods
|
||||
"""
|
||||
rv = set(dir(type(self)))
|
||||
rv = (rv - self._dir_deletions()) | self._dir_additions()
|
||||
return sorted(rv)
|
||||
|
||||
|
||||
class PandasDelegate(object):
|
||||
""" an abstract base class for delegating methods/properties """
|
||||
|
||||
def _delegate_property_get(self, name, *args, **kwargs):
|
||||
raise TypeError("You cannot access the "
|
||||
"property {name}".format(name=name))
|
||||
|
||||
def _delegate_property_set(self, name, value, *args, **kwargs):
|
||||
raise TypeError("The property {name} cannot be set".format(name=name))
|
||||
|
||||
def _delegate_method(self, name, *args, **kwargs):
|
||||
raise TypeError("You cannot call method {name}".format(name=name))
|
||||
|
||||
@classmethod
|
||||
def _add_delegate_accessors(cls, delegate, accessors, typ,
|
||||
overwrite=False):
|
||||
"""
|
||||
add accessors to cls from the delegate class
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cls : the class to add the methods/properties to
|
||||
delegate : the class to get methods/properties & doc-strings
|
||||
acccessors : string list of accessors to add
|
||||
typ : 'property' or 'method'
|
||||
overwrite : boolean, default False
|
||||
overwrite the method/property in the target class if it exists
|
||||
"""
|
||||
|
||||
def _create_delegator_property(name):
|
||||
|
||||
def _getter(self):
|
||||
return self._delegate_property_get(name)
|
||||
|
||||
def _setter(self, new_values):
|
||||
return self._delegate_property_set(name, new_values)
|
||||
|
||||
_getter.__name__ = name
|
||||
_setter.__name__ = name
|
||||
|
||||
return property(fget=_getter, fset=_setter,
|
||||
doc=getattr(delegate, name).__doc__)
|
||||
|
||||
def _create_delegator_method(name):
|
||||
|
||||
def f(self, *args, **kwargs):
|
||||
return self._delegate_method(name, *args, **kwargs)
|
||||
|
||||
f.__name__ = name
|
||||
f.__doc__ = getattr(delegate, name).__doc__
|
||||
|
||||
return f
|
||||
|
||||
for name in accessors:
|
||||
|
||||
if typ == 'property':
|
||||
f = _create_delegator_property(name)
|
||||
else:
|
||||
f = _create_delegator_method(name)
|
||||
|
||||
# don't overwrite existing methods/properties
|
||||
if overwrite or not hasattr(cls, name):
|
||||
setattr(cls, name, f)
|
||||
|
||||
|
||||
# Ported with modifications from xarray
|
||||
# https://github.com/pydata/xarray/blob/master/xarray/core/extensions.py
|
||||
# 1. We don't need to catch and re-raise AttributeErrors as RuntimeErrors
|
||||
# 2. We use a UserWarning instead of a custom Warning
|
||||
|
||||
class CachedAccessor(object):
|
||||
"""Custom property-like object (descriptor) for caching accessors.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The namespace this will be accessed under, e.g. ``df.foo``
|
||||
accessor : cls
|
||||
The class with the extension methods. The class' __init__ method
|
||||
should expect one of a ``Series``, ``DataFrame`` or ``Index`` as
|
||||
the single argument ``data``
|
||||
"""
|
||||
def __init__(self, name, accessor):
|
||||
self._name = name
|
||||
self._accessor = accessor
|
||||
|
||||
def __get__(self, obj, cls):
|
||||
if obj is None:
|
||||
# we're accessing the attribute of the class, i.e., Dataset.geo
|
||||
return self._accessor
|
||||
accessor_obj = self._accessor(obj)
|
||||
# Replace the property with the accessor object. Inspired by:
|
||||
# http://www.pydanny.com/cached-property.html
|
||||
# We need to use object.__setattr__ because we overwrite __setattr__ on
|
||||
# NDFrame
|
||||
object.__setattr__(obj, self._name, accessor_obj)
|
||||
return accessor_obj
|
||||
|
||||
|
||||
def _register_accessor(name, cls):
|
||||
def decorator(accessor):
|
||||
if hasattr(cls, name):
|
||||
warnings.warn(
|
||||
'registration of accessor {!r} under name {!r} for type '
|
||||
'{!r} is overriding a preexisting attribute with the same '
|
||||
'name.'.format(accessor, name, cls),
|
||||
UserWarning,
|
||||
stacklevel=2)
|
||||
setattr(cls, name, CachedAccessor(name, accessor))
|
||||
cls._accessors.add(name)
|
||||
return accessor
|
||||
return decorator
|
||||
|
||||
|
||||
_doc = """Register a custom accessor on %(klass)s objects.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
Name under which the accessor should be registered. A warning is issued
|
||||
if this name conflicts with a preexisting attribute.
|
||||
|
||||
Notes
|
||||
-----
|
||||
When accessed, your accessor will be initialized with the pandas object
|
||||
the user is interacting with. So the signature must be
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def __init__(self, pandas_object):
|
||||
|
||||
For consistency with pandas methods, you should raise an ``AttributeError``
|
||||
if the data passed to your accessor has an incorrect dtype.
|
||||
|
||||
>>> pd.Series(['a', 'b']).dt
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AttributeError: Can only use .dt accessor with datetimelike values
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
In your library code::
|
||||
|
||||
import pandas as pd
|
||||
|
||||
@pd.api.extensions.register_dataframe_accessor("geo")
|
||||
class GeoAccessor(object):
|
||||
def __init__(self, pandas_obj):
|
||||
self._obj = pandas_obj
|
||||
|
||||
@property
|
||||
def center(self):
|
||||
# return the geographic center point of this DataFrame
|
||||
lat = self._obj.latitude
|
||||
lon = self._obj.longitude
|
||||
return (float(lon.mean()), float(lat.mean()))
|
||||
|
||||
def plot(self):
|
||||
# plot this array's data on a map, e.g., using Cartopy
|
||||
pass
|
||||
|
||||
Back in an interactive IPython session:
|
||||
|
||||
>>> ds = pd.DataFrame({'longitude': np.linspace(0, 10),
|
||||
... 'latitude': np.linspace(0, 20)})
|
||||
>>> ds.geo.center
|
||||
(5.0, 10.0)
|
||||
>>> ds.geo.plot()
|
||||
# plots data on a map
|
||||
|
||||
See also
|
||||
--------
|
||||
%(others)s
|
||||
"""
|
||||
|
||||
|
||||
@Appender(_doc % dict(klass="DataFrame",
|
||||
others=("register_series_accessor, "
|
||||
"register_index_accessor")))
|
||||
def register_dataframe_accessor(name):
|
||||
from pandas import DataFrame
|
||||
return _register_accessor(name, DataFrame)
|
||||
|
||||
|
||||
@Appender(_doc % dict(klass="Series",
|
||||
others=("register_dataframe_accessor, "
|
||||
"register_index_accessor")))
|
||||
def register_series_accessor(name):
|
||||
from pandas import Series
|
||||
return _register_accessor(name, Series)
|
||||
|
||||
|
||||
@Appender(_doc % dict(klass="Index",
|
||||
others=("register_dataframe_accessor, "
|
||||
"register_series_accessor")))
|
||||
def register_index_accessor(name):
|
||||
from pandas import Index
|
||||
return _register_accessor(name, Index)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,80 @@
|
||||
|
||||
# pylint: disable=W0614,W0401,W0611
|
||||
# flake8: noqa
|
||||
|
||||
import numpy as np
|
||||
|
||||
from pandas.core.algorithms import factorize, unique, value_counts
|
||||
from pandas.core.dtypes.missing import isna, isnull, notna, notnull
|
||||
from pandas.core.arrays import Categorical
|
||||
from pandas.core.groupby.groupby import Grouper
|
||||
from pandas.io.formats.format import set_eng_float_format
|
||||
from pandas.core.index import (Index, CategoricalIndex, Int64Index,
|
||||
UInt64Index, RangeIndex, Float64Index,
|
||||
MultiIndex, IntervalIndex,
|
||||
TimedeltaIndex, DatetimeIndex,
|
||||
PeriodIndex, NaT)
|
||||
from pandas.core.indexes.period import Period, period_range, pnow
|
||||
from pandas.core.indexes.timedeltas import Timedelta, timedelta_range
|
||||
from pandas.core.indexes.datetimes import Timestamp, date_range, bdate_range
|
||||
from pandas.core.indexes.interval import Interval, interval_range
|
||||
|
||||
from pandas.core.series import Series
|
||||
from pandas.core.frame import DataFrame
|
||||
from pandas.core.panel import Panel, WidePanel
|
||||
|
||||
# TODO: Remove import when statsmodels updates #18264
|
||||
from pandas.core.reshape.reshape import get_dummies
|
||||
|
||||
from pandas.core.indexing import IndexSlice
|
||||
from pandas.core.tools.numeric import to_numeric
|
||||
from pandas.tseries.offsets import DateOffset
|
||||
from pandas.core.tools.datetimes import to_datetime
|
||||
from pandas.core.tools.timedeltas import to_timedelta
|
||||
|
||||
# see gh-14094.
|
||||
from pandas.util._depr_module import _DeprecatedModule
|
||||
|
||||
_removals = ['day', 'bday', 'businessDay', 'cday', 'customBusinessDay',
|
||||
'customBusinessMonthEnd', 'customBusinessMonthBegin',
|
||||
'monthEnd', 'yearEnd', 'yearBegin', 'bmonthEnd', 'bmonthBegin',
|
||||
'cbmonthEnd', 'cbmonthBegin', 'bquarterEnd', 'quarterEnd',
|
||||
'byearEnd', 'week']
|
||||
datetools = _DeprecatedModule(deprmod='pandas.core.datetools',
|
||||
removals=_removals)
|
||||
|
||||
from pandas.core.config import (get_option, set_option, reset_option,
|
||||
describe_option, option_context, options)
|
||||
|
||||
|
||||
# deprecation, xref #13790
|
||||
def match(*args, **kwargs):
|
||||
|
||||
import warnings
|
||||
warnings.warn("pd.match() is deprecated and will be removed "
|
||||
"in a future version",
|
||||
FutureWarning, stacklevel=2)
|
||||
from pandas.core.algorithms import match
|
||||
return match(*args, **kwargs)
|
||||
|
||||
|
||||
def groupby(*args, **kwargs):
|
||||
import warnings
|
||||
|
||||
warnings.warn("pd.groupby() is deprecated and will be removed; "
|
||||
"Please use the Series.groupby() or "
|
||||
"DataFrame.groupby() methods",
|
||||
FutureWarning, stacklevel=2)
|
||||
return args[0].groupby(*args[1:], **kwargs)
|
||||
|
||||
|
||||
# Deprecation: xref gh-16747
|
||||
class TimeGrouper(object):
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
from pandas.core.resample import TimeGrouper
|
||||
import warnings
|
||||
warnings.warn("pd.TimeGrouper is deprecated and will be removed; "
|
||||
"Please use pd.Grouper(freq=...)",
|
||||
FutureWarning, stacklevel=2)
|
||||
return TimeGrouper(*args, **kwargs)
|
||||
@@ -0,0 +1,411 @@
|
||||
import warnings
|
||||
import numpy as np
|
||||
from pandas import compat
|
||||
from pandas._libs import reduction
|
||||
from pandas.core.dtypes.generic import ABCSeries
|
||||
from pandas.core.dtypes.common import (
|
||||
is_extension_type,
|
||||
is_sequence)
|
||||
from pandas.util._decorators import cache_readonly
|
||||
|
||||
from pandas.io.formats.printing import pprint_thing
|
||||
|
||||
|
||||
def frame_apply(obj, func, axis=0, broadcast=None,
|
||||
raw=False, reduce=None, result_type=None,
|
||||
ignore_failures=False,
|
||||
args=None, kwds=None):
|
||||
""" construct and return a row or column based frame apply object """
|
||||
|
||||
axis = obj._get_axis_number(axis)
|
||||
if axis == 0:
|
||||
klass = FrameRowApply
|
||||
elif axis == 1:
|
||||
klass = FrameColumnApply
|
||||
|
||||
return klass(obj, func, broadcast=broadcast,
|
||||
raw=raw, reduce=reduce, result_type=result_type,
|
||||
ignore_failures=ignore_failures,
|
||||
args=args, kwds=kwds)
|
||||
|
||||
|
||||
class FrameApply(object):
|
||||
|
||||
def __init__(self, obj, func, broadcast, raw, reduce, result_type,
|
||||
ignore_failures, args, kwds):
|
||||
self.obj = obj
|
||||
self.raw = raw
|
||||
self.ignore_failures = ignore_failures
|
||||
self.args = args or ()
|
||||
self.kwds = kwds or {}
|
||||
|
||||
if result_type not in [None, 'reduce', 'broadcast', 'expand']:
|
||||
raise ValueError("invalid value for result_type, must be one "
|
||||
"of {None, 'reduce', 'broadcast', 'expand'}")
|
||||
|
||||
if broadcast is not None:
|
||||
warnings.warn("The broadcast argument is deprecated and will "
|
||||
"be removed in a future version. You can specify "
|
||||
"result_type='broadcast' to broadcast the result "
|
||||
"to the original dimensions",
|
||||
FutureWarning, stacklevel=4)
|
||||
if broadcast:
|
||||
result_type = 'broadcast'
|
||||
|
||||
if reduce is not None:
|
||||
warnings.warn("The reduce argument is deprecated and will "
|
||||
"be removed in a future version. You can specify "
|
||||
"result_type='reduce' to try to reduce the result "
|
||||
"to the original dimensions",
|
||||
FutureWarning, stacklevel=4)
|
||||
if reduce:
|
||||
|
||||
if result_type is not None:
|
||||
raise ValueError(
|
||||
"cannot pass both reduce=True and result_type")
|
||||
|
||||
result_type = 'reduce'
|
||||
|
||||
self.result_type = result_type
|
||||
|
||||
# curry if needed
|
||||
if kwds or args and not isinstance(func, np.ufunc):
|
||||
def f(x):
|
||||
return func(x, *args, **kwds)
|
||||
else:
|
||||
f = func
|
||||
|
||||
self.f = f
|
||||
|
||||
# results
|
||||
self.result = None
|
||||
self.res_index = None
|
||||
self.res_columns = None
|
||||
|
||||
@property
|
||||
def columns(self):
|
||||
return self.obj.columns
|
||||
|
||||
@property
|
||||
def index(self):
|
||||
return self.obj.index
|
||||
|
||||
@cache_readonly
|
||||
def values(self):
|
||||
return self.obj.values
|
||||
|
||||
@cache_readonly
|
||||
def dtypes(self):
|
||||
return self.obj.dtypes
|
||||
|
||||
@property
|
||||
def agg_axis(self):
|
||||
return self.obj._get_agg_axis(self.axis)
|
||||
|
||||
def get_result(self):
|
||||
""" compute the results """
|
||||
|
||||
# all empty
|
||||
if len(self.columns) == 0 and len(self.index) == 0:
|
||||
return self.apply_empty_result()
|
||||
|
||||
# string dispatch
|
||||
if isinstance(self.f, compat.string_types):
|
||||
# Support for `frame.transform('method')`
|
||||
# Some methods (shift, etc.) require the axis argument, others
|
||||
# don't, so inspect and insert if nescessary.
|
||||
func = getattr(self.obj, self.f)
|
||||
sig = compat.signature(func)
|
||||
if 'axis' in sig.args:
|
||||
self.kwds['axis'] = self.axis
|
||||
return func(*self.args, **self.kwds)
|
||||
|
||||
# ufunc
|
||||
elif isinstance(self.f, np.ufunc):
|
||||
with np.errstate(all='ignore'):
|
||||
results = self.f(self.values)
|
||||
return self.obj._constructor(data=results, index=self.index,
|
||||
columns=self.columns, copy=False)
|
||||
|
||||
# broadcasting
|
||||
if self.result_type == 'broadcast':
|
||||
return self.apply_broadcast()
|
||||
|
||||
# one axis empty
|
||||
elif not all(self.obj.shape):
|
||||
return self.apply_empty_result()
|
||||
|
||||
# raw
|
||||
elif self.raw and not self.obj._is_mixed_type:
|
||||
return self.apply_raw()
|
||||
|
||||
return self.apply_standard()
|
||||
|
||||
def apply_empty_result(self):
|
||||
"""
|
||||
we have an empty result; at least 1 axis is 0
|
||||
|
||||
we will try to apply the function to an empty
|
||||
series in order to see if this is a reduction function
|
||||
"""
|
||||
|
||||
# we are not asked to reduce or infer reduction
|
||||
# so just return a copy of the existing object
|
||||
if self.result_type not in ['reduce', None]:
|
||||
return self.obj.copy()
|
||||
|
||||
# we may need to infer
|
||||
reduce = self.result_type == 'reduce'
|
||||
|
||||
from pandas import Series
|
||||
if not reduce:
|
||||
|
||||
EMPTY_SERIES = Series([])
|
||||
try:
|
||||
r = self.f(EMPTY_SERIES, *self.args, **self.kwds)
|
||||
reduce = not isinstance(r, Series)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if reduce:
|
||||
return self.obj._constructor_sliced(np.nan, index=self.agg_axis)
|
||||
else:
|
||||
return self.obj.copy()
|
||||
|
||||
def apply_raw(self):
|
||||
""" apply to the values as a numpy array """
|
||||
|
||||
try:
|
||||
result = reduction.reduce(self.values, self.f, axis=self.axis)
|
||||
except Exception:
|
||||
result = np.apply_along_axis(self.f, self.axis, self.values)
|
||||
|
||||
# TODO: mixed type case
|
||||
if result.ndim == 2:
|
||||
return self.obj._constructor(result,
|
||||
index=self.index,
|
||||
columns=self.columns)
|
||||
else:
|
||||
return self.obj._constructor_sliced(result,
|
||||
index=self.agg_axis)
|
||||
|
||||
def apply_broadcast(self, target):
|
||||
result_values = np.empty_like(target.values)
|
||||
|
||||
# axis which we want to compare compliance
|
||||
result_compare = target.shape[0]
|
||||
|
||||
for i, col in enumerate(target.columns):
|
||||
res = self.f(target[col])
|
||||
ares = np.asarray(res).ndim
|
||||
|
||||
# must be a scalar or 1d
|
||||
if ares > 1:
|
||||
raise ValueError("too many dims to broadcast")
|
||||
elif ares == 1:
|
||||
|
||||
# must match return dim
|
||||
if result_compare != len(res):
|
||||
raise ValueError("cannot broadcast result")
|
||||
|
||||
result_values[:, i] = res
|
||||
|
||||
# we *always* preserve the original index / columns
|
||||
result = self.obj._constructor(result_values,
|
||||
index=target.index,
|
||||
columns=target.columns)
|
||||
return result
|
||||
|
||||
def apply_standard(self):
|
||||
|
||||
# try to reduce first (by default)
|
||||
# this only matters if the reduction in values is of different dtype
|
||||
# e.g. if we want to apply to a SparseFrame, then can't directly reduce
|
||||
|
||||
# we cannot reduce using non-numpy dtypes,
|
||||
# as demonstrated in gh-12244
|
||||
if (self.result_type in ['reduce', None] and
|
||||
not self.dtypes.apply(is_extension_type).any()):
|
||||
|
||||
# Create a dummy Series from an empty array
|
||||
from pandas import Series
|
||||
values = self.values
|
||||
index = self.obj._get_axis(self.axis)
|
||||
labels = self.agg_axis
|
||||
empty_arr = np.empty(len(index), dtype=values.dtype)
|
||||
dummy = Series(empty_arr, index=index, dtype=values.dtype)
|
||||
|
||||
try:
|
||||
result = reduction.reduce(values, self.f,
|
||||
axis=self.axis,
|
||||
dummy=dummy,
|
||||
labels=labels)
|
||||
return self.obj._constructor_sliced(result, index=labels)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# compute the result using the series generator
|
||||
self.apply_series_generator()
|
||||
|
||||
# wrap results
|
||||
return self.wrap_results()
|
||||
|
||||
def apply_series_generator(self):
|
||||
series_gen = self.series_generator
|
||||
res_index = self.result_index
|
||||
|
||||
i = None
|
||||
keys = []
|
||||
results = {}
|
||||
if self.ignore_failures:
|
||||
successes = []
|
||||
for i, v in enumerate(series_gen):
|
||||
try:
|
||||
results[i] = self.f(v)
|
||||
keys.append(v.name)
|
||||
successes.append(i)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# so will work with MultiIndex
|
||||
if len(successes) < len(res_index):
|
||||
res_index = res_index.take(successes)
|
||||
|
||||
else:
|
||||
try:
|
||||
for i, v in enumerate(series_gen):
|
||||
results[i] = self.f(v)
|
||||
keys.append(v.name)
|
||||
except Exception as e:
|
||||
if hasattr(e, 'args'):
|
||||
|
||||
# make sure i is defined
|
||||
if i is not None:
|
||||
k = res_index[i]
|
||||
e.args = e.args + ('occurred at index %s' %
|
||||
pprint_thing(k), )
|
||||
raise
|
||||
|
||||
self.results = results
|
||||
self.res_index = res_index
|
||||
self.res_columns = self.result_columns
|
||||
|
||||
def wrap_results(self):
|
||||
results = self.results
|
||||
|
||||
# see if we can infer the results
|
||||
if len(results) > 0 and is_sequence(results[0]):
|
||||
|
||||
return self.wrap_results_for_axis()
|
||||
|
||||
# dict of scalars
|
||||
result = self.obj._constructor_sliced(results)
|
||||
result.index = self.res_index
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class FrameRowApply(FrameApply):
|
||||
axis = 0
|
||||
|
||||
def get_result(self):
|
||||
|
||||
# dispatch to agg
|
||||
if isinstance(self.f, (list, dict)):
|
||||
return self.obj.aggregate(self.f, axis=self.axis,
|
||||
*self.args, **self.kwds)
|
||||
|
||||
return super(FrameRowApply, self).get_result()
|
||||
|
||||
def apply_broadcast(self):
|
||||
return super(FrameRowApply, self).apply_broadcast(self.obj)
|
||||
|
||||
@property
|
||||
def series_generator(self):
|
||||
return (self.obj._ixs(i, axis=1)
|
||||
for i in range(len(self.columns)))
|
||||
|
||||
@property
|
||||
def result_index(self):
|
||||
return self.columns
|
||||
|
||||
@property
|
||||
def result_columns(self):
|
||||
return self.index
|
||||
|
||||
def wrap_results_for_axis(self):
|
||||
""" return the results for the rows """
|
||||
|
||||
results = self.results
|
||||
result = self.obj._constructor(data=results)
|
||||
|
||||
if not isinstance(results[0], ABCSeries):
|
||||
try:
|
||||
result.index = self.res_columns
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
result.columns = self.res_index
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class FrameColumnApply(FrameApply):
|
||||
axis = 1
|
||||
|
||||
def apply_broadcast(self):
|
||||
result = super(FrameColumnApply, self).apply_broadcast(self.obj.T)
|
||||
return result.T
|
||||
|
||||
@property
|
||||
def series_generator(self):
|
||||
constructor = self.obj._constructor_sliced
|
||||
return (constructor(arr, index=self.columns, name=name)
|
||||
for i, (arr, name) in enumerate(zip(self.values,
|
||||
self.index)))
|
||||
|
||||
@property
|
||||
def result_index(self):
|
||||
return self.index
|
||||
|
||||
@property
|
||||
def result_columns(self):
|
||||
return self.columns
|
||||
|
||||
def wrap_results_for_axis(self):
|
||||
""" return the results for the columns """
|
||||
results = self.results
|
||||
|
||||
# we have requested to expand
|
||||
if self.result_type == 'expand':
|
||||
result = self.infer_to_same_shape()
|
||||
|
||||
# we have a non-series and don't want inference
|
||||
elif not isinstance(results[0], ABCSeries):
|
||||
from pandas import Series
|
||||
result = Series(results)
|
||||
result.index = self.res_index
|
||||
|
||||
# we may want to infer results
|
||||
else:
|
||||
result = self.infer_to_same_shape()
|
||||
|
||||
return result
|
||||
|
||||
def infer_to_same_shape(self):
|
||||
""" infer the results to the same shape as the input object """
|
||||
results = self.results
|
||||
|
||||
result = self.obj._constructor(data=results)
|
||||
result = result.T
|
||||
|
||||
# set the index
|
||||
result.index = self.res_index
|
||||
|
||||
# infer dtypes
|
||||
result = result.infer_objects()
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,2 @@
|
||||
from .base import ExtensionArray # noqa
|
||||
from .categorical import Categorical # noqa
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,612 @@
|
||||
"""An interface for extending pandas with custom arrays.
|
||||
|
||||
.. warning::
|
||||
|
||||
This is an experimental API and subject to breaking changes
|
||||
without warning.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
from pandas.errors import AbstractMethodError
|
||||
from pandas.compat.numpy import function as nv
|
||||
|
||||
_not_implemented_message = "{} does not implement {}."
|
||||
|
||||
|
||||
class ExtensionArray(object):
|
||||
"""Abstract base class for custom 1-D array types.
|
||||
|
||||
pandas will recognize instances of this class as proper arrays
|
||||
with a custom type and will not attempt to coerce them to objects. They
|
||||
may be stored directly inside a :class:`DataFrame` or :class:`Series`.
|
||||
|
||||
.. versionadded:: 0.23.0
|
||||
|
||||
Notes
|
||||
-----
|
||||
The interface includes the following abstract methods that must be
|
||||
implemented by subclasses:
|
||||
|
||||
* _from_sequence
|
||||
* _from_factorized
|
||||
* __getitem__
|
||||
* __len__
|
||||
* dtype
|
||||
* nbytes
|
||||
* isna
|
||||
* take
|
||||
* copy
|
||||
* _concat_same_type
|
||||
|
||||
An additional method is available to satisfy pandas' internal,
|
||||
private block API.
|
||||
|
||||
* _formatting_values
|
||||
|
||||
Some methods require casting the ExtensionArray to an ndarray of Python
|
||||
objects with ``self.astype(object)``, which may be expensive. When
|
||||
performance is a concern, we highly recommend overriding the following
|
||||
methods:
|
||||
|
||||
* fillna
|
||||
* unique
|
||||
* factorize / _values_for_factorize
|
||||
* argsort / _values_for_argsort
|
||||
|
||||
This class does not inherit from 'abc.ABCMeta' for performance reasons.
|
||||
Methods and properties required by the interface raise
|
||||
``pandas.errors.AbstractMethodError`` and no ``register`` method is
|
||||
provided for registering virtual subclasses.
|
||||
|
||||
ExtensionArrays are limited to 1 dimension.
|
||||
|
||||
They may be backed by none, one, or many NumPy arrays. For example,
|
||||
``pandas.Categorical`` is an extension array backed by two arrays,
|
||||
one for codes and one for categories. An array of IPv6 address may
|
||||
be backed by a NumPy structured array with two fields, one for the
|
||||
lower 64 bits and one for the upper 64 bits. Or they may be backed
|
||||
by some other storage type, like Python lists. Pandas makes no
|
||||
assumptions on how the data are stored, just that it can be converted
|
||||
to a NumPy array.
|
||||
The ExtensionArray interface does not impose any rules on how this data
|
||||
is stored. However, currently, the backing data cannot be stored in
|
||||
attributes called ``.values`` or ``._values`` to ensure full compatibility
|
||||
with pandas internals. But other names as ``.data``, ``._data``,
|
||||
``._items``, ... can be freely used.
|
||||
"""
|
||||
# '_typ' is for pandas.core.dtypes.generic.ABCExtensionArray.
|
||||
# Don't override this.
|
||||
_typ = 'extension'
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Constructors
|
||||
# ------------------------------------------------------------------------
|
||||
@classmethod
|
||||
def _from_sequence(cls, scalars):
|
||||
"""Construct a new ExtensionArray from a sequence of scalars.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
scalars : Sequence
|
||||
Each element will be an instance of the scalar type for this
|
||||
array, ``cls.dtype.type``.
|
||||
Returns
|
||||
-------
|
||||
ExtensionArray
|
||||
"""
|
||||
raise AbstractMethodError(cls)
|
||||
|
||||
@classmethod
|
||||
def _from_factorized(cls, values, original):
|
||||
"""Reconstruct an ExtensionArray after factorization.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
values : ndarray
|
||||
An integer ndarray with the factorized values.
|
||||
original : ExtensionArray
|
||||
The original ExtensionArray that factorize was called on.
|
||||
|
||||
See Also
|
||||
--------
|
||||
pandas.factorize
|
||||
ExtensionArray.factorize
|
||||
"""
|
||||
raise AbstractMethodError(cls)
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Must be a Sequence
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
def __getitem__(self, item):
|
||||
# type (Any) -> Any
|
||||
"""Select a subset of self.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item : int, slice, or ndarray
|
||||
* int: The position in 'self' to get.
|
||||
|
||||
* slice: A slice object, where 'start', 'stop', and 'step' are
|
||||
integers or None
|
||||
|
||||
* ndarray: A 1-d boolean NumPy ndarray the same length as 'self'
|
||||
|
||||
Returns
|
||||
-------
|
||||
item : scalar or ExtensionArray
|
||||
|
||||
Notes
|
||||
-----
|
||||
For scalar ``item``, return a scalar value suitable for the array's
|
||||
type. This should be an instance of ``self.dtype.type``.
|
||||
|
||||
For slice ``key``, return an instance of ``ExtensionArray``, even
|
||||
if the slice is length 0 or 1.
|
||||
|
||||
For a boolean mask, return an instance of ``ExtensionArray``, filtered
|
||||
to the values where ``item`` is True.
|
||||
"""
|
||||
raise AbstractMethodError(self)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
# type: (Union[int, np.ndarray], Any) -> None
|
||||
"""Set one or more values inplace.
|
||||
|
||||
This method is not required to satisfy the pandas extension array
|
||||
interface.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
key : int, ndarray, or slice
|
||||
When called from, e.g. ``Series.__setitem__``, ``key`` will be
|
||||
one of
|
||||
|
||||
* scalar int
|
||||
* ndarray of integers.
|
||||
* boolean ndarray
|
||||
* slice object
|
||||
|
||||
value : ExtensionDtype.type, Sequence[ExtensionDtype.type], or object
|
||||
value or values to be set of ``key``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
# Some notes to the ExtensionArray implementor who may have ended up
|
||||
# here. While this method is not required for the interface, if you
|
||||
# *do* choose to implement __setitem__, then some semantics should be
|
||||
# observed:
|
||||
#
|
||||
# * Setting multiple values : ExtensionArrays should support setting
|
||||
# multiple values at once, 'key' will be a sequence of integers and
|
||||
# 'value' will be a same-length sequence.
|
||||
#
|
||||
# * Broadcasting : For a sequence 'key' and a scalar 'value',
|
||||
# each position in 'key' should be set to 'value'.
|
||||
#
|
||||
# * Coercion : Most users will expect basic coercion to work. For
|
||||
# example, a string like '2018-01-01' is coerced to a datetime
|
||||
# when setting on a datetime64ns array. In general, if the
|
||||
# __init__ method coerces that value, then so should __setitem__
|
||||
raise NotImplementedError(_not_implemented_message.format(
|
||||
type(self), '__setitem__')
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""Length of this array
|
||||
|
||||
Returns
|
||||
-------
|
||||
length : int
|
||||
"""
|
||||
# type: () -> int
|
||||
raise AbstractMethodError(self)
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over elements of the array.
|
||||
|
||||
"""
|
||||
# This needs to be implemented so that pandas recognizes extension
|
||||
# arrays as list-like. The default implementation makes successive
|
||||
# calls to ``__getitem__``, which may be slower than necessary.
|
||||
for i in range(len(self)):
|
||||
yield self[i]
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Required attributes
|
||||
# ------------------------------------------------------------------------
|
||||
@property
|
||||
def dtype(self):
|
||||
# type: () -> ExtensionDtype
|
||||
"""An instance of 'ExtensionDtype'."""
|
||||
raise AbstractMethodError(self)
|
||||
|
||||
@property
|
||||
def shape(self):
|
||||
# type: () -> Tuple[int, ...]
|
||||
"""Return a tuple of the array dimensions."""
|
||||
return (len(self),)
|
||||
|
||||
@property
|
||||
def ndim(self):
|
||||
# type: () -> int
|
||||
"""Extension Arrays are only allowed to be 1-dimensional."""
|
||||
return 1
|
||||
|
||||
@property
|
||||
def nbytes(self):
|
||||
# type: () -> int
|
||||
"""The number of bytes needed to store this object in memory.
|
||||
|
||||
"""
|
||||
# If this is expensive to compute, return an approximate lower bound
|
||||
# on the number of bytes needed.
|
||||
raise AbstractMethodError(self)
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Additional Methods
|
||||
# ------------------------------------------------------------------------
|
||||
def astype(self, dtype, copy=True):
|
||||
"""Cast to a NumPy array with 'dtype'.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dtype : str or dtype
|
||||
Typecode or data-type to which the array is cast.
|
||||
copy : bool, default True
|
||||
Whether to copy the data, even if not necessary. If False,
|
||||
a copy is made only if the old dtype does not match the
|
||||
new dtype.
|
||||
|
||||
Returns
|
||||
-------
|
||||
array : ndarray
|
||||
NumPy ndarray with 'dtype' for its dtype.
|
||||
"""
|
||||
return np.array(self, dtype=dtype, copy=copy)
|
||||
|
||||
def isna(self):
|
||||
# type: () -> np.ndarray
|
||||
"""Boolean NumPy array indicating if each value is missing.
|
||||
|
||||
This should return a 1-D array the same length as 'self'.
|
||||
"""
|
||||
raise AbstractMethodError(self)
|
||||
|
||||
def _values_for_argsort(self):
|
||||
# type: () -> ndarray
|
||||
"""Return values for sorting.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ndarray
|
||||
The transformed values should maintain the ordering between values
|
||||
within the array.
|
||||
|
||||
See Also
|
||||
--------
|
||||
ExtensionArray.argsort
|
||||
"""
|
||||
# Note: this is used in `ExtensionArray.argsort`.
|
||||
return np.array(self)
|
||||
|
||||
def argsort(self, ascending=True, kind='quicksort', *args, **kwargs):
|
||||
"""
|
||||
Return the indices that would sort this array.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ascending : bool, default True
|
||||
Whether the indices should result in an ascending
|
||||
or descending sort.
|
||||
kind : {'quicksort', 'mergesort', 'heapsort'}, optional
|
||||
Sorting algorithm.
|
||||
*args, **kwargs:
|
||||
passed through to :func:`numpy.argsort`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
index_array : ndarray
|
||||
Array of indices that sort ``self``.
|
||||
|
||||
See Also
|
||||
--------
|
||||
numpy.argsort : Sorting implementation used internally.
|
||||
"""
|
||||
# Implementor note: You have two places to override the behavior of
|
||||
# argsort.
|
||||
# 1. _values_for_argsort : construct the values passed to np.argsort
|
||||
# 2. argsort : total control over sorting.
|
||||
ascending = nv.validate_argsort_with_ascending(ascending, args, kwargs)
|
||||
values = self._values_for_argsort()
|
||||
result = np.argsort(values, kind=kind, **kwargs)
|
||||
if not ascending:
|
||||
result = result[::-1]
|
||||
return result
|
||||
|
||||
def fillna(self, value=None, method=None, limit=None):
|
||||
""" Fill NA/NaN values using the specified method.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : scalar, array-like
|
||||
If a scalar value is passed it is used to fill all missing values.
|
||||
Alternatively, an array-like 'value' can be given. It's expected
|
||||
that the array-like have the same length as 'self'.
|
||||
method : {'backfill', 'bfill', 'pad', 'ffill', None}, default None
|
||||
Method to use for filling holes in reindexed Series
|
||||
pad / ffill: propagate last valid observation forward to next valid
|
||||
backfill / bfill: use NEXT valid observation to fill gap
|
||||
limit : int, default None
|
||||
If method is specified, this is the maximum number of consecutive
|
||||
NaN values to forward/backward fill. In other words, if there is
|
||||
a gap with more than this number of consecutive NaNs, it will only
|
||||
be partially filled. If method is not specified, this is the
|
||||
maximum number of entries along the entire axis where NaNs will be
|
||||
filled.
|
||||
|
||||
Returns
|
||||
-------
|
||||
filled : ExtensionArray with NA/NaN filled
|
||||
"""
|
||||
from pandas.api.types import is_array_like
|
||||
from pandas.util._validators import validate_fillna_kwargs
|
||||
from pandas.core.missing import pad_1d, backfill_1d
|
||||
|
||||
value, method = validate_fillna_kwargs(value, method)
|
||||
|
||||
mask = self.isna()
|
||||
|
||||
if is_array_like(value):
|
||||
if len(value) != len(self):
|
||||
raise ValueError("Length of 'value' does not match. Got ({}) "
|
||||
" expected {}".format(len(value), len(self)))
|
||||
value = value[mask]
|
||||
|
||||
if mask.any():
|
||||
if method is not None:
|
||||
func = pad_1d if method == 'pad' else backfill_1d
|
||||
new_values = func(self.astype(object), limit=limit,
|
||||
mask=mask)
|
||||
new_values = self._from_sequence(new_values)
|
||||
else:
|
||||
# fill with value
|
||||
new_values = self.copy()
|
||||
new_values[mask] = value
|
||||
else:
|
||||
new_values = self.copy()
|
||||
return new_values
|
||||
|
||||
def unique(self):
|
||||
"""Compute the ExtensionArray of unique values.
|
||||
|
||||
Returns
|
||||
-------
|
||||
uniques : ExtensionArray
|
||||
"""
|
||||
from pandas import unique
|
||||
|
||||
uniques = unique(self.astype(object))
|
||||
return self._from_sequence(uniques)
|
||||
|
||||
def _values_for_factorize(self):
|
||||
# type: () -> Tuple[ndarray, Any]
|
||||
"""Return an array and missing value suitable for factorization.
|
||||
|
||||
Returns
|
||||
-------
|
||||
values : ndarray
|
||||
|
||||
An array suitable for factorization. This should maintain order
|
||||
and be a supported dtype (Float64, Int64, UInt64, String, Object).
|
||||
By default, the extension array is cast to object dtype.
|
||||
na_value : object
|
||||
The value in `values` to consider missing. This will be treated
|
||||
as NA in the factorization routines, so it will be coded as
|
||||
`na_sentinal` and not included in `uniques`. By default,
|
||||
``np.nan`` is used.
|
||||
"""
|
||||
return self.astype(object), np.nan
|
||||
|
||||
def factorize(self, na_sentinel=-1):
|
||||
# type: (int) -> Tuple[ndarray, ExtensionArray]
|
||||
"""Encode the extension array as an enumerated type.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
na_sentinel : int, default -1
|
||||
Value to use in the `labels` array to indicate missing values.
|
||||
|
||||
Returns
|
||||
-------
|
||||
labels : ndarray
|
||||
An integer NumPy array that's an indexer into the original
|
||||
ExtensionArray.
|
||||
uniques : ExtensionArray
|
||||
An ExtensionArray containing the unique values of `self`.
|
||||
|
||||
.. note::
|
||||
|
||||
uniques will *not* contain an entry for the NA value of
|
||||
the ExtensionArray if there are any missing values present
|
||||
in `self`.
|
||||
|
||||
See Also
|
||||
--------
|
||||
pandas.factorize : Top-level factorize method that dispatches here.
|
||||
|
||||
Notes
|
||||
-----
|
||||
:meth:`pandas.factorize` offers a `sort` keyword as well.
|
||||
"""
|
||||
# Impelmentor note: There are two ways to override the behavior of
|
||||
# pandas.factorize
|
||||
# 1. _values_for_factorize and _from_factorize.
|
||||
# Specify the values passed to pandas' internal factorization
|
||||
# routines, and how to convert from those values back to the
|
||||
# original ExtensionArray.
|
||||
# 2. ExtensionArray.factorize.
|
||||
# Complete control over factorization.
|
||||
from pandas.core.algorithms import _factorize_array
|
||||
|
||||
arr, na_value = self._values_for_factorize()
|
||||
|
||||
labels, uniques = _factorize_array(arr, na_sentinel=na_sentinel,
|
||||
na_value=na_value)
|
||||
|
||||
uniques = self._from_factorized(uniques, self)
|
||||
return labels, uniques
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Indexing methods
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
def take(self, indices, allow_fill=False, fill_value=None):
|
||||
# type: (Sequence[int], bool, Optional[Any]) -> ExtensionArray
|
||||
"""Take elements from an array.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
indices : sequence of integers
|
||||
Indices to be taken.
|
||||
allow_fill : bool, default False
|
||||
How to handle negative values in `indices`.
|
||||
|
||||
* False: negative values in `indices` indicate positional indices
|
||||
from the right (the default). This is similar to
|
||||
:func:`numpy.take`.
|
||||
|
||||
* True: negative values in `indices` indicate
|
||||
missing values. These values are set to `fill_value`. Any other
|
||||
other negative values raise a ``ValueError``.
|
||||
|
||||
fill_value : any, optional
|
||||
Fill value to use for NA-indices when `allow_fill` is True.
|
||||
This may be ``None``, in which case the default NA value for
|
||||
the type, ``self.dtype.na_value``, is used.
|
||||
|
||||
For many ExtensionArrays, there will be two representations of
|
||||
`fill_value`: a user-facing "boxed" scalar, and a low-level
|
||||
physical NA value. `fill_value` should be the user-facing version,
|
||||
and the implementation should handle translating that to the
|
||||
physical version for processing the take if nescessary.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ExtensionArray
|
||||
|
||||
Raises
|
||||
------
|
||||
IndexError
|
||||
When the indices are out of bounds for the array.
|
||||
ValueError
|
||||
When `indices` contains negative values other than ``-1``
|
||||
and `allow_fill` is True.
|
||||
|
||||
Notes
|
||||
-----
|
||||
ExtensionArray.take is called by ``Series.__getitem__``, ``.loc``,
|
||||
``iloc``, when `indices` is a sequence of values. Additionally,
|
||||
it's called by :meth:`Series.reindex`, or any other method
|
||||
that causes realignemnt, with a `fill_value`.
|
||||
|
||||
See Also
|
||||
--------
|
||||
numpy.take
|
||||
pandas.api.extensions.take
|
||||
|
||||
Examples
|
||||
--------
|
||||
Here's an example implementation, which relies on casting the
|
||||
extension array to object dtype. This uses the helper method
|
||||
:func:`pandas.api.extensions.take`.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def take(self, indices, allow_fill=False, fill_value=None):
|
||||
from pandas.core.algorithms import take
|
||||
|
||||
# If the ExtensionArray is backed by an ndarray, then
|
||||
# just pass that here instead of coercing to object.
|
||||
data = self.astype(object)
|
||||
|
||||
if allow_fill and fill_value is None:
|
||||
fill_value = self.dtype.na_value
|
||||
|
||||
# fill value should always be translated from the scalar
|
||||
# type for the array, to the physical storage type for
|
||||
# the data, before passing to take.
|
||||
|
||||
result = take(data, indices, fill_value=fill_value,
|
||||
allow_fill=allow_fill)
|
||||
return self._from_sequence(result)
|
||||
"""
|
||||
# Implementer note: The `fill_value` parameter should be a user-facing
|
||||
# value, an instance of self.dtype.type. When passed `fill_value=None`,
|
||||
# the default of `self.dtype.na_value` should be used.
|
||||
# This may differ from the physical storage type your ExtensionArray
|
||||
# uses. In this case, your implementation is responsible for casting
|
||||
# the user-facing type to the storage type, before using
|
||||
# pandas.api.extensions.take
|
||||
raise AbstractMethodError(self)
|
||||
|
||||
def copy(self, deep=False):
|
||||
# type: (bool) -> ExtensionArray
|
||||
"""Return a copy of the array.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
deep : bool, default False
|
||||
Also copy the underlying data backing this array.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ExtensionArray
|
||||
"""
|
||||
raise AbstractMethodError(self)
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Block-related methods
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
def _formatting_values(self):
|
||||
# type: () -> np.ndarray
|
||||
# At the moment, this has to be an array since we use result.dtype
|
||||
"""An array of values to be printed in, e.g. the Series repr"""
|
||||
return np.array(self)
|
||||
|
||||
@classmethod
|
||||
def _concat_same_type(cls, to_concat):
|
||||
# type: (Sequence[ExtensionArray]) -> ExtensionArray
|
||||
"""Concatenate multiple array
|
||||
|
||||
Parameters
|
||||
----------
|
||||
to_concat : sequence of this type
|
||||
|
||||
Returns
|
||||
-------
|
||||
ExtensionArray
|
||||
"""
|
||||
raise AbstractMethodError(cls)
|
||||
|
||||
# The _can_hold_na attribute is set to True so that pandas internals
|
||||
# will use the ExtensionDtype.na_value as the NA value in operations
|
||||
# such as take(), reindex(), shift(), etc. In addition, those results
|
||||
# will then be of the ExtensionArray subclass rather than an array
|
||||
# of objects
|
||||
_can_hold_na = True
|
||||
|
||||
@property
|
||||
def _ndarray_values(self):
|
||||
# type: () -> np.ndarray
|
||||
"""Internal pandas method for lossy conversion to a NumPy ndarray.
|
||||
|
||||
This method is not part of the pandas interface.
|
||||
|
||||
The expectation is that this is cheap to compute, and is primarily
|
||||
used for interacting with our indexers.
|
||||
"""
|
||||
return np.array(self)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
import warnings
|
||||
|
||||
# TODO: Remove after 0.23.x
|
||||
warnings.warn("'pandas.core' is private. Use 'pandas.Categorical'",
|
||||
FutureWarning, stacklevel=2)
|
||||
|
||||
from pandas.core.arrays import Categorical # noqa
|
||||
from pandas.core.dtypes.dtypes import CategoricalDtype # noqa
|
||||
@@ -0,0 +1,632 @@
|
||||
"""
|
||||
Misc tools for implementing data structures
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
import inspect
|
||||
import collections
|
||||
|
||||
import numpy as np
|
||||
from pandas._libs import lib, tslib
|
||||
|
||||
from pandas import compat
|
||||
from pandas.compat import long, zip, iteritems, PY36, OrderedDict
|
||||
from pandas.core.config import get_option
|
||||
from pandas.core.dtypes.generic import ABCSeries, ABCIndex
|
||||
from pandas.core.dtypes.common import _NS_DTYPE, is_integer
|
||||
from pandas.core.dtypes.inference import _iterable_not_string
|
||||
from pandas.core.dtypes.missing import isna, isnull, notnull # noqa
|
||||
from pandas.core.dtypes.cast import construct_1d_object_array_from_listlike
|
||||
|
||||
|
||||
class SettingWithCopyError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class SettingWithCopyWarning(Warning):
|
||||
pass
|
||||
|
||||
|
||||
def flatten(l):
|
||||
"""Flatten an arbitrarily nested sequence.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
l : sequence
|
||||
The non string sequence to flatten
|
||||
|
||||
Notes
|
||||
-----
|
||||
This doesn't consider strings sequences.
|
||||
|
||||
Returns
|
||||
-------
|
||||
flattened : generator
|
||||
"""
|
||||
for el in l:
|
||||
if _iterable_not_string(el):
|
||||
for s in flatten(el):
|
||||
yield s
|
||||
else:
|
||||
yield el
|
||||
|
||||
|
||||
def _consensus_name_attr(objs):
|
||||
name = objs[0].name
|
||||
for obj in objs[1:]:
|
||||
try:
|
||||
if obj.name != name:
|
||||
name = None
|
||||
except ValueError:
|
||||
name = None
|
||||
return name
|
||||
|
||||
|
||||
def _get_info_slice(obj, indexer):
|
||||
"""Slice the info axis of `obj` with `indexer`."""
|
||||
if not hasattr(obj, '_info_axis_number'):
|
||||
msg = 'object of type {typ!r} has no info axis'
|
||||
raise TypeError(msg.format(typ=type(obj).__name__))
|
||||
slices = [slice(None)] * obj.ndim
|
||||
slices[obj._info_axis_number] = indexer
|
||||
return tuple(slices)
|
||||
|
||||
|
||||
def _maybe_box(indexer, values, obj, key):
|
||||
|
||||
# if we have multiples coming back, box em
|
||||
if isinstance(values, np.ndarray):
|
||||
return obj[indexer.get_loc(key)]
|
||||
|
||||
# return the value
|
||||
return values
|
||||
|
||||
|
||||
def _maybe_box_datetimelike(value):
|
||||
# turn a datetime like into a Timestamp/timedelta as needed
|
||||
|
||||
if isinstance(value, (np.datetime64, datetime)):
|
||||
value = tslib.Timestamp(value)
|
||||
elif isinstance(value, (np.timedelta64, timedelta)):
|
||||
value = tslib.Timedelta(value)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
_values_from_object = lib.values_from_object
|
||||
|
||||
|
||||
def is_bool_indexer(key):
|
||||
if isinstance(key, (ABCSeries, np.ndarray, ABCIndex)):
|
||||
if key.dtype == np.object_:
|
||||
key = np.asarray(_values_from_object(key))
|
||||
|
||||
if not lib.is_bool_array(key):
|
||||
if isna(key).any():
|
||||
raise ValueError('cannot index with vector containing '
|
||||
'NA / NaN values')
|
||||
return False
|
||||
return True
|
||||
elif key.dtype == np.bool_:
|
||||
return True
|
||||
elif isinstance(key, list):
|
||||
try:
|
||||
arr = np.asarray(key)
|
||||
return arr.dtype == np.bool_ and len(arr) == len(key)
|
||||
except TypeError: # pragma: no cover
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _default_index(n):
|
||||
from pandas.core.index import RangeIndex
|
||||
return RangeIndex(0, n, name=None)
|
||||
|
||||
|
||||
def _mut_exclusive(**kwargs):
|
||||
item1, item2 = kwargs.items()
|
||||
label1, val1 = item1
|
||||
label2, val2 = item2
|
||||
if val1 is not None and val2 is not None:
|
||||
msg = 'mutually exclusive arguments: {label1!r} and {label2!r}'
|
||||
raise TypeError(msg.format(label1=label1, label2=label2))
|
||||
elif val1 is not None:
|
||||
return val1
|
||||
else:
|
||||
return val2
|
||||
|
||||
|
||||
def _not_none(*args):
|
||||
"""Returns a generator consisting of the arguments that are not None"""
|
||||
return (arg for arg in args if arg is not None)
|
||||
|
||||
|
||||
def _any_none(*args):
|
||||
"""Returns a boolean indicating if any argument is None"""
|
||||
for arg in args:
|
||||
if arg is None:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _all_none(*args):
|
||||
"""Returns a boolean indicating if all arguments are None"""
|
||||
for arg in args:
|
||||
if arg is not None:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _any_not_none(*args):
|
||||
"""Returns a boolean indicating if any argument is not None"""
|
||||
for arg in args:
|
||||
if arg is not None:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _all_not_none(*args):
|
||||
"""Returns a boolean indicating if all arguments are not None"""
|
||||
for arg in args:
|
||||
if arg is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _count_not_none(*args):
|
||||
"""Returns the count of arguments that are not None"""
|
||||
return sum(x is not None for x in args)
|
||||
|
||||
|
||||
def _try_sort(iterable):
|
||||
listed = list(iterable)
|
||||
try:
|
||||
return sorted(listed)
|
||||
except Exception:
|
||||
return listed
|
||||
|
||||
|
||||
def _dict_keys_to_ordered_list(mapping):
|
||||
# when pandas drops support for Python < 3.6, this function
|
||||
# can be replaced by a simple list(mapping.keys())
|
||||
if PY36 or isinstance(mapping, OrderedDict):
|
||||
keys = list(mapping.keys())
|
||||
else:
|
||||
keys = _try_sort(mapping)
|
||||
return keys
|
||||
|
||||
|
||||
def iterpairs(seq):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
seq : sequence
|
||||
|
||||
Returns
|
||||
-------
|
||||
iterator returning overlapping pairs of elements
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> list(iterpairs([1, 2, 3, 4]))
|
||||
[(1, 2), (2, 3), (3, 4)]
|
||||
"""
|
||||
# input may not be sliceable
|
||||
seq_it = iter(seq)
|
||||
seq_it_next = iter(seq)
|
||||
next(seq_it_next)
|
||||
|
||||
return zip(seq_it, seq_it_next)
|
||||
|
||||
|
||||
def split_ranges(mask):
|
||||
""" Generates tuples of ranges which cover all True value in mask
|
||||
|
||||
>>> list(split_ranges([1,0,0,1,0]))
|
||||
[(0, 1), (3, 4)]
|
||||
"""
|
||||
ranges = [(0, len(mask))]
|
||||
|
||||
for pos, val in enumerate(mask):
|
||||
if not val: # this pos should be omitted, split off the prefix range
|
||||
r = ranges.pop()
|
||||
if pos > r[0]: # yield non-zero range
|
||||
yield (r[0], pos)
|
||||
if pos + 1 < len(mask): # save the rest for processing
|
||||
ranges.append((pos + 1, len(mask)))
|
||||
if ranges:
|
||||
yield ranges[-1]
|
||||
|
||||
|
||||
def _long_prod(vals):
|
||||
result = long(1)
|
||||
for x in vals:
|
||||
result *= x
|
||||
return result
|
||||
|
||||
|
||||
class groupby(dict):
|
||||
"""
|
||||
A simple groupby different from the one in itertools.
|
||||
|
||||
Does not require the sequence elements to be sorted by keys,
|
||||
however it is slower.
|
||||
"""
|
||||
|
||||
def __init__(self, seq, key=lambda x: x):
|
||||
for value in seq:
|
||||
k = key(value)
|
||||
self.setdefault(k, []).append(value)
|
||||
|
||||
try:
|
||||
__iter__ = dict.iteritems
|
||||
except AttributeError: # pragma: no cover
|
||||
# Python 3
|
||||
def __iter__(self):
|
||||
return iter(dict.items(self))
|
||||
|
||||
|
||||
def map_indices_py(arr):
|
||||
"""
|
||||
Returns a dictionary with (element, index) pairs for each element in the
|
||||
given array/list
|
||||
"""
|
||||
return {x: i for i, x in enumerate(arr)}
|
||||
|
||||
|
||||
def union(*seqs):
|
||||
result = set([])
|
||||
for seq in seqs:
|
||||
if not isinstance(seq, set):
|
||||
seq = set(seq)
|
||||
result |= seq
|
||||
return type(seqs[0])(list(result))
|
||||
|
||||
|
||||
def difference(a, b):
|
||||
return type(a)(list(set(a) - set(b)))
|
||||
|
||||
|
||||
def intersection(*seqs):
|
||||
result = set(seqs[0])
|
||||
for seq in seqs:
|
||||
if not isinstance(seq, set):
|
||||
seq = set(seq)
|
||||
result &= seq
|
||||
return type(seqs[0])(list(result))
|
||||
|
||||
|
||||
def _asarray_tuplesafe(values, dtype=None):
|
||||
from pandas.core.index import Index
|
||||
|
||||
if not (isinstance(values, (list, tuple)) or hasattr(values, '__array__')):
|
||||
values = list(values)
|
||||
elif isinstance(values, Index):
|
||||
return values.values
|
||||
|
||||
if isinstance(values, list) and dtype in [np.object_, object]:
|
||||
return construct_1d_object_array_from_listlike(values)
|
||||
|
||||
result = np.asarray(values, dtype=dtype)
|
||||
|
||||
if issubclass(result.dtype.type, compat.string_types):
|
||||
result = np.asarray(values, dtype=object)
|
||||
|
||||
if result.ndim == 2:
|
||||
# Avoid building an array of arrays:
|
||||
# TODO: verify whether any path hits this except #18819 (invalid)
|
||||
values = [tuple(x) for x in values]
|
||||
result = construct_1d_object_array_from_listlike(values)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _index_labels_to_array(labels, dtype=None):
|
||||
"""
|
||||
Transform label or iterable of labels to array, for use in Index.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dtype : dtype
|
||||
If specified, use as dtype of the resulting array, otherwise infer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
array
|
||||
"""
|
||||
if isinstance(labels, (compat.string_types, tuple)):
|
||||
labels = [labels]
|
||||
|
||||
if not isinstance(labels, (list, np.ndarray)):
|
||||
try:
|
||||
labels = list(labels)
|
||||
except TypeError: # non-iterable
|
||||
labels = [labels]
|
||||
|
||||
labels = _asarray_tuplesafe(labels, dtype=dtype)
|
||||
|
||||
return labels
|
||||
|
||||
|
||||
def _maybe_make_list(obj):
|
||||
if obj is not None and not isinstance(obj, (tuple, list)):
|
||||
return [obj]
|
||||
return obj
|
||||
|
||||
|
||||
def is_null_slice(obj):
|
||||
""" we have a null slice """
|
||||
return (isinstance(obj, slice) and obj.start is None and
|
||||
obj.stop is None and obj.step is None)
|
||||
|
||||
|
||||
def is_true_slices(l):
|
||||
"""
|
||||
Find non-trivial slices in "l": return a list of booleans with same length.
|
||||
"""
|
||||
return [isinstance(k, slice) and not is_null_slice(k) for k in l]
|
||||
|
||||
|
||||
def is_full_slice(obj, l):
|
||||
""" we have a full length slice """
|
||||
return (isinstance(obj, slice) and obj.start == 0 and obj.stop == l and
|
||||
obj.step is None)
|
||||
|
||||
|
||||
def _get_callable_name(obj):
|
||||
# typical case has name
|
||||
if hasattr(obj, '__name__'):
|
||||
return getattr(obj, '__name__')
|
||||
# some objects don't; could recurse
|
||||
if isinstance(obj, partial):
|
||||
return _get_callable_name(obj.func)
|
||||
# fall back to class name
|
||||
if hasattr(obj, '__call__'):
|
||||
return obj.__class__.__name__
|
||||
# everything failed (probably because the argument
|
||||
# wasn't actually callable); we return None
|
||||
# instead of the empty string in this case to allow
|
||||
# distinguishing between no name and a name of ''
|
||||
return None
|
||||
|
||||
|
||||
def _apply_if_callable(maybe_callable, obj, **kwargs):
|
||||
"""
|
||||
Evaluate possibly callable input using obj and kwargs if it is callable,
|
||||
otherwise return as it is
|
||||
|
||||
Parameters
|
||||
----------
|
||||
maybe_callable : possibly a callable
|
||||
obj : NDFrame
|
||||
**kwargs
|
||||
"""
|
||||
|
||||
if callable(maybe_callable):
|
||||
return maybe_callable(obj, **kwargs)
|
||||
|
||||
return maybe_callable
|
||||
|
||||
|
||||
def _where_compat(mask, arr1, arr2):
|
||||
if arr1.dtype == _NS_DTYPE and arr2.dtype == _NS_DTYPE:
|
||||
new_vals = np.where(mask, arr1.view('i8'), arr2.view('i8'))
|
||||
return new_vals.view(_NS_DTYPE)
|
||||
|
||||
if arr1.dtype == _NS_DTYPE:
|
||||
arr1 = tslib.ints_to_pydatetime(arr1.view('i8'))
|
||||
if arr2.dtype == _NS_DTYPE:
|
||||
arr2 = tslib.ints_to_pydatetime(arr2.view('i8'))
|
||||
|
||||
return np.where(mask, arr1, arr2)
|
||||
|
||||
|
||||
def _dict_compat(d):
|
||||
"""
|
||||
Helper function to convert datetimelike-keyed dicts to Timestamp-keyed dict
|
||||
|
||||
Parameters
|
||||
----------
|
||||
d: dict like object
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
|
||||
"""
|
||||
return dict((_maybe_box_datetimelike(key), value)
|
||||
for key, value in iteritems(d))
|
||||
|
||||
|
||||
def standardize_mapping(into):
|
||||
"""
|
||||
Helper function to standardize a supplied mapping.
|
||||
|
||||
.. versionadded:: 0.21.0
|
||||
|
||||
Parameters
|
||||
----------
|
||||
into : instance or subclass of collections.Mapping
|
||||
Must be a class, an initialized collections.defaultdict,
|
||||
or an instance of a collections.Mapping subclass.
|
||||
|
||||
Returns
|
||||
-------
|
||||
mapping : a collections.Mapping subclass or other constructor
|
||||
a callable object that can accept an iterator to create
|
||||
the desired Mapping.
|
||||
|
||||
See Also
|
||||
--------
|
||||
DataFrame.to_dict
|
||||
Series.to_dict
|
||||
"""
|
||||
if not inspect.isclass(into):
|
||||
if isinstance(into, collections.defaultdict):
|
||||
return partial(
|
||||
collections.defaultdict, into.default_factory)
|
||||
into = type(into)
|
||||
if not issubclass(into, collections.Mapping):
|
||||
raise TypeError('unsupported type: {into}'.format(into=into))
|
||||
elif into == collections.defaultdict:
|
||||
raise TypeError(
|
||||
'to_dict() only accepts initialized defaultdicts')
|
||||
return into
|
||||
|
||||
|
||||
def sentinel_factory():
|
||||
class Sentinel(object):
|
||||
pass
|
||||
|
||||
return Sentinel()
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Detect our environment
|
||||
|
||||
def in_interactive_session():
|
||||
""" check if we're running in an interactive shell
|
||||
|
||||
returns True if running under python/ipython interactive shell
|
||||
"""
|
||||
|
||||
def check_main():
|
||||
import __main__ as main
|
||||
return (not hasattr(main, '__file__') or
|
||||
get_option('mode.sim_interactive'))
|
||||
|
||||
try:
|
||||
return __IPYTHON__ or check_main() # noqa
|
||||
except:
|
||||
return check_main()
|
||||
|
||||
|
||||
def in_qtconsole():
|
||||
"""
|
||||
check if we're inside an IPython qtconsole
|
||||
|
||||
.. deprecated:: 0.14.1
|
||||
This is no longer needed, or working, in IPython 3 and above.
|
||||
"""
|
||||
try:
|
||||
ip = get_ipython() # noqa
|
||||
front_end = (
|
||||
ip.config.get('KernelApp', {}).get('parent_appname', "") or
|
||||
ip.config.get('IPKernelApp', {}).get('parent_appname', ""))
|
||||
if 'qtconsole' in front_end.lower():
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def in_ipnb():
|
||||
"""
|
||||
check if we're inside an IPython Notebook
|
||||
|
||||
.. deprecated:: 0.14.1
|
||||
This is no longer needed, or working, in IPython 3 and above.
|
||||
"""
|
||||
try:
|
||||
ip = get_ipython() # noqa
|
||||
front_end = (
|
||||
ip.config.get('KernelApp', {}).get('parent_appname', "") or
|
||||
ip.config.get('IPKernelApp', {}).get('parent_appname', ""))
|
||||
if 'notebook' in front_end.lower():
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def in_ipython_frontend():
|
||||
"""
|
||||
check if we're inside an an IPython zmq frontend
|
||||
"""
|
||||
try:
|
||||
ip = get_ipython() # noqa
|
||||
return 'zmq' in str(type(ip)).lower()
|
||||
except:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _random_state(state=None):
|
||||
"""
|
||||
Helper function for processing random_state arguments.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : int, np.random.RandomState, None.
|
||||
If receives an int, passes to np.random.RandomState() as seed.
|
||||
If receives an np.random.RandomState object, just returns object.
|
||||
If receives `None`, returns np.random.
|
||||
If receives anything else, raises an informative ValueError.
|
||||
Default None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.random.RandomState
|
||||
"""
|
||||
|
||||
if is_integer(state):
|
||||
return np.random.RandomState(state)
|
||||
elif isinstance(state, np.random.RandomState):
|
||||
return state
|
||||
elif state is None:
|
||||
return np.random
|
||||
else:
|
||||
raise ValueError("random_state must be an integer, a numpy "
|
||||
"RandomState, or None")
|
||||
|
||||
|
||||
def _get_distinct_objs(objs):
|
||||
"""
|
||||
Return a list with distinct elements of "objs" (different ids).
|
||||
Preserves order.
|
||||
"""
|
||||
ids = set()
|
||||
res = []
|
||||
for obj in objs:
|
||||
if not id(obj) in ids:
|
||||
ids.add(id(obj))
|
||||
res.append(obj)
|
||||
return res
|
||||
|
||||
|
||||
def _pipe(obj, func, *args, **kwargs):
|
||||
"""
|
||||
Apply a function ``func`` to object ``obj`` either by passing obj as the
|
||||
first argument to the function or, in the case that the func is a tuple,
|
||||
interpret the first element of the tuple as a function and pass the obj to
|
||||
that function as a keyword argument whose key is the value of the second
|
||||
element of the tuple.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
func : callable or tuple of (callable, string)
|
||||
Function to apply to this object or, alternatively, a
|
||||
``(callable, data_keyword)`` tuple where ``data_keyword`` is a
|
||||
string indicating the keyword of `callable`` that expects the
|
||||
object.
|
||||
args : iterable, optional
|
||||
positional arguments passed into ``func``.
|
||||
kwargs : dict, optional
|
||||
a dictionary of keyword arguments passed into ``func``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
object : the return type of ``func``.
|
||||
"""
|
||||
if isinstance(func, tuple):
|
||||
func, target = func
|
||||
if target in kwargs:
|
||||
msg = '%s is both the pipe target and a keyword argument' % target
|
||||
raise ValueError(msg)
|
||||
kwargs[target] = obj
|
||||
return func(*args, **kwargs)
|
||||
else:
|
||||
return func(obj, *args, **kwargs)
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,179 @@
|
||||
"""Core eval alignment algorithms
|
||||
"""
|
||||
|
||||
import warnings
|
||||
from functools import partial, wraps
|
||||
from pandas.compat import zip, range
|
||||
|
||||
import numpy as np
|
||||
|
||||
import pandas as pd
|
||||
from pandas import compat
|
||||
from pandas.errors import PerformanceWarning
|
||||
import pandas.core.common as com
|
||||
from pandas.core.computation.common import _result_type_many
|
||||
|
||||
|
||||
def _align_core_single_unary_op(term):
|
||||
if isinstance(term.value, np.ndarray):
|
||||
typ = partial(np.asanyarray, dtype=term.value.dtype)
|
||||
else:
|
||||
typ = type(term.value)
|
||||
ret = typ,
|
||||
|
||||
if not hasattr(term.value, 'axes'):
|
||||
ret += None,
|
||||
else:
|
||||
ret += _zip_axes_from_type(typ, term.value.axes),
|
||||
return ret
|
||||
|
||||
|
||||
def _zip_axes_from_type(typ, new_axes):
|
||||
axes = {}
|
||||
for ax_ind, ax_name in compat.iteritems(typ._AXIS_NAMES):
|
||||
axes[ax_name] = new_axes[ax_ind]
|
||||
return axes
|
||||
|
||||
|
||||
def _any_pandas_objects(terms):
|
||||
"""Check a sequence of terms for instances of PandasObject."""
|
||||
return any(isinstance(term.value, pd.core.generic.PandasObject)
|
||||
for term in terms)
|
||||
|
||||
|
||||
def _filter_special_cases(f):
|
||||
@wraps(f)
|
||||
def wrapper(terms):
|
||||
# single unary operand
|
||||
if len(terms) == 1:
|
||||
return _align_core_single_unary_op(terms[0])
|
||||
|
||||
term_values = (term.value for term in terms)
|
||||
|
||||
# we don't have any pandas objects
|
||||
if not _any_pandas_objects(terms):
|
||||
return _result_type_many(*term_values), None
|
||||
|
||||
return f(terms)
|
||||
return wrapper
|
||||
|
||||
|
||||
@_filter_special_cases
|
||||
def _align_core(terms):
|
||||
term_index = [i for i, term in enumerate(terms)
|
||||
if hasattr(term.value, 'axes')]
|
||||
term_dims = [terms[i].value.ndim for i in term_index]
|
||||
ndims = pd.Series(dict(zip(term_index, term_dims)))
|
||||
|
||||
# initial axes are the axes of the largest-axis'd term
|
||||
biggest = terms[ndims.idxmax()].value
|
||||
typ = biggest._constructor
|
||||
axes = biggest.axes
|
||||
naxes = len(axes)
|
||||
gt_than_one_axis = naxes > 1
|
||||
|
||||
for value in (terms[i].value for i in term_index):
|
||||
is_series = isinstance(value, pd.Series)
|
||||
is_series_and_gt_one_axis = is_series and gt_than_one_axis
|
||||
|
||||
for axis, items in enumerate(value.axes):
|
||||
if is_series_and_gt_one_axis:
|
||||
ax, itm = naxes - 1, value.index
|
||||
else:
|
||||
ax, itm = axis, items
|
||||
|
||||
if not axes[ax].is_(itm):
|
||||
axes[ax] = axes[ax].join(itm, how='outer')
|
||||
|
||||
for i, ndim in compat.iteritems(ndims):
|
||||
for axis, items in zip(range(ndim), axes):
|
||||
ti = terms[i].value
|
||||
|
||||
if hasattr(ti, 'reindex'):
|
||||
transpose = isinstance(ti, pd.Series) and naxes > 1
|
||||
reindexer = axes[naxes - 1] if transpose else items
|
||||
|
||||
term_axis_size = len(ti.axes[axis])
|
||||
reindexer_size = len(reindexer)
|
||||
|
||||
ordm = np.log10(max(1, abs(reindexer_size - term_axis_size)))
|
||||
if ordm >= 1 and reindexer_size >= 10000:
|
||||
w = ('Alignment difference on axis {axis} is larger '
|
||||
'than an order of magnitude on term {term!r}, by '
|
||||
'more than {ordm:.4g}; performance may suffer'
|
||||
).format(axis=axis, term=terms[i].name, ordm=ordm)
|
||||
warnings.warn(w, category=PerformanceWarning, stacklevel=6)
|
||||
|
||||
f = partial(ti.reindex, reindexer, axis=axis, copy=False)
|
||||
|
||||
terms[i].update(f())
|
||||
|
||||
terms[i].update(terms[i].value.values)
|
||||
|
||||
return typ, _zip_axes_from_type(typ, axes)
|
||||
|
||||
|
||||
def _align(terms):
|
||||
"""Align a set of terms"""
|
||||
try:
|
||||
# flatten the parse tree (a nested list, really)
|
||||
terms = list(com.flatten(terms))
|
||||
except TypeError:
|
||||
# can't iterate so it must just be a constant or single variable
|
||||
if isinstance(terms.value, pd.core.generic.NDFrame):
|
||||
typ = type(terms.value)
|
||||
return typ, _zip_axes_from_type(typ, terms.value.axes)
|
||||
return np.result_type(terms.type), None
|
||||
|
||||
# if all resolved variables are numeric scalars
|
||||
if all(term.is_scalar for term in terms):
|
||||
return _result_type_many(*(term.value for term in terms)).type, None
|
||||
|
||||
# perform the main alignment
|
||||
typ, axes = _align_core(terms)
|
||||
return typ, axes
|
||||
|
||||
|
||||
def _reconstruct_object(typ, obj, axes, dtype):
|
||||
"""Reconstruct an object given its type, raw value, and possibly empty
|
||||
(None) axes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
typ : object
|
||||
A type
|
||||
obj : object
|
||||
The value to use in the type constructor
|
||||
axes : dict
|
||||
The axes to use to construct the resulting pandas object
|
||||
|
||||
Returns
|
||||
-------
|
||||
ret : typ
|
||||
An object of type ``typ`` with the value `obj` and possible axes
|
||||
`axes`.
|
||||
"""
|
||||
try:
|
||||
typ = typ.type
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
res_t = np.result_type(obj.dtype, dtype)
|
||||
|
||||
if (not isinstance(typ, partial) and
|
||||
issubclass(typ, pd.core.generic.PandasObject)):
|
||||
return typ(obj, dtype=res_t, **axes)
|
||||
|
||||
# special case for pathological things like ~True/~False
|
||||
if hasattr(res_t, 'type') and typ == np.bool_ and res_t != np.bool_:
|
||||
ret_value = res_t.type(obj)
|
||||
else:
|
||||
ret_value = typ(obj).astype(res_t)
|
||||
# The condition is to distinguish 0-dim array (returned in case of
|
||||
# scalar) and 1 element array
|
||||
# e.g. np.array(0) and np.array([0])
|
||||
if len(obj.shape) == 1 and len(obj) == 1:
|
||||
if not isinstance(ret_value, np.ndarray):
|
||||
ret_value = np.array([ret_value]).astype(res_t)
|
||||
|
||||
return ret_value
|
||||
@@ -0,0 +1,14 @@
|
||||
# flake8: noqa
|
||||
|
||||
from pandas.core.computation.eval import eval
|
||||
|
||||
|
||||
# deprecation, xref #13790
|
||||
def Expr(*args, **kwargs):
|
||||
import warnings
|
||||
|
||||
warnings.warn("pd.Expr is deprecated as it is not "
|
||||
"applicable to user code",
|
||||
FutureWarning, stacklevel=2)
|
||||
from pandas.core.computation.expr import Expr
|
||||
return Expr(*args, **kwargs)
|
||||
@@ -0,0 +1,22 @@
|
||||
import warnings
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
_NUMEXPR_INSTALLED = False
|
||||
_MIN_NUMEXPR_VERSION = "2.4.6"
|
||||
|
||||
try:
|
||||
import numexpr as ne
|
||||
ver = LooseVersion(ne.__version__)
|
||||
_NUMEXPR_INSTALLED = ver >= LooseVersion(_MIN_NUMEXPR_VERSION)
|
||||
|
||||
if not _NUMEXPR_INSTALLED:
|
||||
warnings.warn(
|
||||
"The installed version of numexpr {ver} is not supported "
|
||||
"in pandas and will be not be used\nThe minimum supported "
|
||||
"version is {min_ver}\n".format(
|
||||
ver=ver, min_ver=_MIN_NUMEXPR_VERSION), UserWarning)
|
||||
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
__all__ = ['_NUMEXPR_INSTALLED']
|
||||
@@ -0,0 +1,24 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from pandas.compat import reduce
|
||||
|
||||
|
||||
def _ensure_decoded(s):
|
||||
""" if we have bytes, decode them to unicode """
|
||||
if isinstance(s, (np.bytes_, bytes)):
|
||||
s = s.decode(pd.get_option('display.encoding'))
|
||||
return s
|
||||
|
||||
|
||||
def _result_type_many(*arrays_and_dtypes):
|
||||
""" wrapper around numpy.result_type which overcomes the NPY_MAXARGS (32)
|
||||
argument limit """
|
||||
try:
|
||||
return np.result_type(*arrays_and_dtypes)
|
||||
except ValueError:
|
||||
# we have > NPY_MAXARGS terms in our expression
|
||||
return reduce(np.result_type, arrays_and_dtypes)
|
||||
|
||||
|
||||
class NameResolutionError(NameError):
|
||||
pass
|
||||
@@ -0,0 +1,151 @@
|
||||
"""
|
||||
Engine classes for :func:`~pandas.eval`
|
||||
"""
|
||||
|
||||
import abc
|
||||
|
||||
from pandas import compat
|
||||
from pandas.compat import map
|
||||
import pandas.io.formats.printing as printing
|
||||
from pandas.core.computation.align import _align, _reconstruct_object
|
||||
from pandas.core.computation.ops import (
|
||||
UndefinedVariableError,
|
||||
_mathops, _reductions)
|
||||
|
||||
|
||||
_ne_builtins = frozenset(_mathops + _reductions)
|
||||
|
||||
|
||||
class NumExprClobberingError(NameError):
|
||||
pass
|
||||
|
||||
|
||||
def _check_ne_builtin_clash(expr):
|
||||
"""Attempt to prevent foot-shooting in a helpful way.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
terms : Term
|
||||
Terms can contain
|
||||
"""
|
||||
names = expr.names
|
||||
overlap = names & _ne_builtins
|
||||
|
||||
if overlap:
|
||||
s = ', '.join(map(repr, overlap))
|
||||
raise NumExprClobberingError('Variables in expression "{expr}" '
|
||||
'overlap with builtins: ({s})'
|
||||
.format(expr=expr, s=s))
|
||||
|
||||
|
||||
class AbstractEngine(object):
|
||||
|
||||
"""Object serving as a base class for all engines."""
|
||||
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
has_neg_frac = False
|
||||
|
||||
def __init__(self, expr):
|
||||
self.expr = expr
|
||||
self.aligned_axes = None
|
||||
self.result_type = None
|
||||
|
||||
def convert(self):
|
||||
"""Convert an expression for evaluation.
|
||||
|
||||
Defaults to return the expression as a string.
|
||||
"""
|
||||
return printing.pprint_thing(self.expr)
|
||||
|
||||
def evaluate(self):
|
||||
"""Run the engine on the expression
|
||||
|
||||
This method performs alignment which is necessary no matter what engine
|
||||
is being used, thus its implementation is in the base class.
|
||||
|
||||
Returns
|
||||
-------
|
||||
obj : object
|
||||
The result of the passed expression.
|
||||
"""
|
||||
if not self._is_aligned:
|
||||
self.result_type, self.aligned_axes = _align(self.expr.terms)
|
||||
|
||||
# make sure no names in resolvers and locals/globals clash
|
||||
res = self._evaluate()
|
||||
return _reconstruct_object(self.result_type, res, self.aligned_axes,
|
||||
self.expr.terms.return_type)
|
||||
|
||||
@property
|
||||
def _is_aligned(self):
|
||||
return self.aligned_axes is not None and self.result_type is not None
|
||||
|
||||
@abc.abstractmethod
|
||||
def _evaluate(self):
|
||||
"""Return an evaluated expression.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
env : Scope
|
||||
The local and global environment in which to evaluate an
|
||||
expression.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Must be implemented by subclasses.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class NumExprEngine(AbstractEngine):
|
||||
|
||||
"""NumExpr engine class"""
|
||||
has_neg_frac = True
|
||||
|
||||
def __init__(self, expr):
|
||||
super(NumExprEngine, self).__init__(expr)
|
||||
|
||||
def convert(self):
|
||||
return str(super(NumExprEngine, self).convert())
|
||||
|
||||
def _evaluate(self):
|
||||
import numexpr as ne
|
||||
|
||||
# convert the expression to a valid numexpr expression
|
||||
s = self.convert()
|
||||
|
||||
try:
|
||||
env = self.expr.env
|
||||
scope = env.full_scope
|
||||
truediv = scope['truediv']
|
||||
_check_ne_builtin_clash(self.expr)
|
||||
return ne.evaluate(s, local_dict=scope, truediv=truediv)
|
||||
except KeyError as e:
|
||||
# python 3 compat kludge
|
||||
try:
|
||||
msg = e.message
|
||||
except AttributeError:
|
||||
msg = compat.text_type(e)
|
||||
raise UndefinedVariableError(msg)
|
||||
|
||||
|
||||
class PythonEngine(AbstractEngine):
|
||||
|
||||
"""Evaluate an expression in Python space.
|
||||
|
||||
Mostly for testing purposes.
|
||||
"""
|
||||
has_neg_frac = False
|
||||
|
||||
def __init__(self, expr):
|
||||
super(PythonEngine, self).__init__(expr)
|
||||
|
||||
def evaluate(self):
|
||||
return self.expr()
|
||||
|
||||
def _evaluate(self):
|
||||
pass
|
||||
|
||||
|
||||
_engines = {'numexpr': NumExprEngine, 'python': PythonEngine}
|
||||
@@ -0,0 +1,347 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""Top level ``eval`` module.
|
||||
"""
|
||||
|
||||
import warnings
|
||||
import tokenize
|
||||
from pandas.io.formats.printing import pprint_thing
|
||||
from pandas.core.computation.scope import _ensure_scope
|
||||
from pandas.compat import string_types
|
||||
from pandas.core.computation.engines import _engines
|
||||
from pandas.util._validators import validate_bool_kwarg
|
||||
|
||||
|
||||
def _check_engine(engine):
|
||||
"""Make sure a valid engine is passed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
engine : str
|
||||
|
||||
Raises
|
||||
------
|
||||
KeyError
|
||||
* If an invalid engine is passed
|
||||
ImportError
|
||||
* If numexpr was requested but doesn't exist
|
||||
|
||||
Returns
|
||||
-------
|
||||
string engine
|
||||
|
||||
"""
|
||||
from pandas.core.computation.check import _NUMEXPR_INSTALLED
|
||||
|
||||
if engine is None:
|
||||
if _NUMEXPR_INSTALLED:
|
||||
engine = 'numexpr'
|
||||
else:
|
||||
engine = 'python'
|
||||
|
||||
if engine not in _engines:
|
||||
valid = list(_engines.keys())
|
||||
raise KeyError('Invalid engine {engine!r} passed, valid engines are'
|
||||
' {valid}'.format(engine=engine, valid=valid))
|
||||
|
||||
# TODO: validate this in a more general way (thinking of future engines
|
||||
# that won't necessarily be import-able)
|
||||
# Could potentially be done on engine instantiation
|
||||
if engine == 'numexpr':
|
||||
if not _NUMEXPR_INSTALLED:
|
||||
raise ImportError("'numexpr' is not installed or an "
|
||||
"unsupported version. Cannot use "
|
||||
"engine='numexpr' for query/eval "
|
||||
"if 'numexpr' is not installed")
|
||||
|
||||
return engine
|
||||
|
||||
|
||||
def _check_parser(parser):
|
||||
"""Make sure a valid parser is passed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
parser : str
|
||||
|
||||
Raises
|
||||
------
|
||||
KeyError
|
||||
* If an invalid parser is passed
|
||||
"""
|
||||
from pandas.core.computation.expr import _parsers
|
||||
|
||||
if parser not in _parsers:
|
||||
raise KeyError('Invalid parser {parser!r} passed, valid parsers are'
|
||||
' {valid}'.format(parser=parser, valid=_parsers.keys()))
|
||||
|
||||
|
||||
def _check_resolvers(resolvers):
|
||||
if resolvers is not None:
|
||||
for resolver in resolvers:
|
||||
if not hasattr(resolver, '__getitem__'):
|
||||
name = type(resolver).__name__
|
||||
raise TypeError('Resolver of type {name!r} does not implement '
|
||||
'the __getitem__ method'.format(name=name))
|
||||
|
||||
|
||||
def _check_expression(expr):
|
||||
"""Make sure an expression is not an empty string
|
||||
|
||||
Parameters
|
||||
----------
|
||||
expr : object
|
||||
An object that can be converted to a string
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
* If expr is an empty string
|
||||
"""
|
||||
if not expr:
|
||||
raise ValueError("expr cannot be an empty string")
|
||||
|
||||
|
||||
def _convert_expression(expr):
|
||||
"""Convert an object to an expression.
|
||||
|
||||
Thus function converts an object to an expression (a unicode string) and
|
||||
checks to make sure it isn't empty after conversion. This is used to
|
||||
convert operators to their string representation for recursive calls to
|
||||
:func:`~pandas.eval`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
expr : object
|
||||
The object to be converted to a string.
|
||||
|
||||
Returns
|
||||
-------
|
||||
s : unicode
|
||||
The string representation of an object.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
* If the expression is empty.
|
||||
"""
|
||||
s = pprint_thing(expr)
|
||||
_check_expression(s)
|
||||
return s
|
||||
|
||||
|
||||
def _check_for_locals(expr, stack_level, parser):
|
||||
from pandas.core.computation.expr import tokenize_string
|
||||
|
||||
at_top_of_stack = stack_level == 0
|
||||
not_pandas_parser = parser != 'pandas'
|
||||
|
||||
if not_pandas_parser:
|
||||
msg = "The '@' prefix is only supported by the pandas parser"
|
||||
elif at_top_of_stack:
|
||||
msg = ("The '@' prefix is not allowed in "
|
||||
"top-level eval calls, \nplease refer to "
|
||||
"your variables by name without the '@' "
|
||||
"prefix")
|
||||
|
||||
if at_top_of_stack or not_pandas_parser:
|
||||
for toknum, tokval in tokenize_string(expr):
|
||||
if toknum == tokenize.OP and tokval == '@':
|
||||
raise SyntaxError(msg)
|
||||
|
||||
|
||||
def eval(expr, parser='pandas', engine=None, truediv=True,
|
||||
local_dict=None, global_dict=None, resolvers=(), level=0,
|
||||
target=None, inplace=False):
|
||||
"""Evaluate a Python expression as a string using various backends.
|
||||
|
||||
The following arithmetic operations are supported: ``+``, ``-``, ``*``,
|
||||
``/``, ``**``, ``%``, ``//`` (python engine only) along with the following
|
||||
boolean operations: ``|`` (or), ``&`` (and), and ``~`` (not).
|
||||
Additionally, the ``'pandas'`` parser allows the use of :keyword:`and`,
|
||||
:keyword:`or`, and :keyword:`not` with the same semantics as the
|
||||
corresponding bitwise operators. :class:`~pandas.Series` and
|
||||
:class:`~pandas.DataFrame` objects are supported and behave as they would
|
||||
with plain ol' Python evaluation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
expr : str or unicode
|
||||
The expression to evaluate. This string cannot contain any Python
|
||||
`statements
|
||||
<https://docs.python.org/3/reference/simple_stmts.html#simple-statements>`__,
|
||||
only Python `expressions
|
||||
<https://docs.python.org/3/reference/simple_stmts.html#expression-statements>`__.
|
||||
parser : string, default 'pandas', {'pandas', 'python'}
|
||||
The parser to use to construct the syntax tree from the expression. The
|
||||
default of ``'pandas'`` parses code slightly different than standard
|
||||
Python. Alternatively, you can parse an expression using the
|
||||
``'python'`` parser to retain strict Python semantics. See the
|
||||
:ref:`enhancing performance <enhancingperf.eval>` documentation for
|
||||
more details.
|
||||
engine : string or None, default 'numexpr', {'python', 'numexpr'}
|
||||
|
||||
The engine used to evaluate the expression. Supported engines are
|
||||
|
||||
- None : tries to use ``numexpr``, falls back to ``python``
|
||||
- ``'numexpr'``: This default engine evaluates pandas objects using
|
||||
numexpr for large speed ups in complex expressions
|
||||
with large frames.
|
||||
- ``'python'``: Performs operations as if you had ``eval``'d in top
|
||||
level python. This engine is generally not that useful.
|
||||
|
||||
More backends may be available in the future.
|
||||
|
||||
truediv : bool, optional
|
||||
Whether to use true division, like in Python >= 3
|
||||
local_dict : dict or None, optional
|
||||
A dictionary of local variables, taken from locals() by default.
|
||||
global_dict : dict or None, optional
|
||||
A dictionary of global variables, taken from globals() by default.
|
||||
resolvers : list of dict-like or None, optional
|
||||
A list of objects implementing the ``__getitem__`` special method that
|
||||
you can use to inject an additional collection of namespaces to use for
|
||||
variable lookup. For example, this is used in the
|
||||
:meth:`~pandas.DataFrame.query` method to inject the
|
||||
``DataFrame.index`` and ``DataFrame.columns``
|
||||
variables that refer to their respective :class:`~pandas.DataFrame`
|
||||
instance attributes.
|
||||
level : int, optional
|
||||
The number of prior stack frames to traverse and add to the current
|
||||
scope. Most users will **not** need to change this parameter.
|
||||
target : object, optional, default None
|
||||
This is the target object for assignment. It is used when there is
|
||||
variable assignment in the expression. If so, then `target` must
|
||||
support item assignment with string keys, and if a copy is being
|
||||
returned, it must also support `.copy()`.
|
||||
inplace : bool, default False
|
||||
If `target` is provided, and the expression mutates `target`, whether
|
||||
to modify `target` inplace. Otherwise, return a copy of `target` with
|
||||
the mutation.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ndarray, numeric scalar, DataFrame, Series
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
There are many instances where such an error can be raised:
|
||||
|
||||
- `target=None`, but the expression is multiline.
|
||||
- The expression is multiline, but not all them have item assignment.
|
||||
An example of such an arrangement is this:
|
||||
|
||||
a = b + 1
|
||||
a + 2
|
||||
|
||||
Here, there are expressions on different lines, making it multiline,
|
||||
but the last line has no variable assigned to the output of `a + 2`.
|
||||
- `inplace=True`, but the expression is missing item assignment.
|
||||
- Item assignment is provided, but the `target` does not support
|
||||
string item assignment.
|
||||
- Item assignment is provided and `inplace=False`, but the `target`
|
||||
does not support the `.copy()` method
|
||||
|
||||
Notes
|
||||
-----
|
||||
The ``dtype`` of any objects involved in an arithmetic ``%`` operation are
|
||||
recursively cast to ``float64``.
|
||||
|
||||
See the :ref:`enhancing performance <enhancingperf.eval>` documentation for
|
||||
more details.
|
||||
|
||||
See Also
|
||||
--------
|
||||
pandas.DataFrame.query
|
||||
pandas.DataFrame.eval
|
||||
"""
|
||||
from pandas.core.computation.expr import Expr
|
||||
|
||||
inplace = validate_bool_kwarg(inplace, "inplace")
|
||||
|
||||
if isinstance(expr, string_types):
|
||||
_check_expression(expr)
|
||||
exprs = [e.strip() for e in expr.splitlines() if e.strip() != '']
|
||||
else:
|
||||
exprs = [expr]
|
||||
multi_line = len(exprs) > 1
|
||||
|
||||
if multi_line and target is None:
|
||||
raise ValueError("multi-line expressions are only valid in the "
|
||||
"context of data, use DataFrame.eval")
|
||||
|
||||
ret = None
|
||||
first_expr = True
|
||||
target_modified = False
|
||||
|
||||
for expr in exprs:
|
||||
expr = _convert_expression(expr)
|
||||
engine = _check_engine(engine)
|
||||
_check_parser(parser)
|
||||
_check_resolvers(resolvers)
|
||||
_check_for_locals(expr, level, parser)
|
||||
|
||||
# get our (possibly passed-in) scope
|
||||
env = _ensure_scope(level + 1, global_dict=global_dict,
|
||||
local_dict=local_dict, resolvers=resolvers,
|
||||
target=target)
|
||||
|
||||
parsed_expr = Expr(expr, engine=engine, parser=parser, env=env,
|
||||
truediv=truediv)
|
||||
|
||||
# construct the engine and evaluate the parsed expression
|
||||
eng = _engines[engine]
|
||||
eng_inst = eng(parsed_expr)
|
||||
ret = eng_inst.evaluate()
|
||||
|
||||
if parsed_expr.assigner is None:
|
||||
if multi_line:
|
||||
raise ValueError("Multi-line expressions are only valid"
|
||||
" if all expressions contain an assignment")
|
||||
elif inplace:
|
||||
raise ValueError("Cannot operate inplace "
|
||||
"if there is no assignment")
|
||||
|
||||
# assign if needed
|
||||
assigner = parsed_expr.assigner
|
||||
if env.target is not None and assigner is not None:
|
||||
target_modified = True
|
||||
|
||||
# if returning a copy, copy only on the first assignment
|
||||
if not inplace and first_expr:
|
||||
try:
|
||||
target = env.target.copy()
|
||||
except AttributeError:
|
||||
raise ValueError("Cannot return a copy of the target")
|
||||
else:
|
||||
target = env.target
|
||||
|
||||
# TypeError is most commonly raised (e.g. int, list), but you
|
||||
# get IndexError if you try to do this assignment on np.ndarray.
|
||||
# we will ignore numpy warnings here; e.g. if trying
|
||||
# to use a non-numeric indexer
|
||||
try:
|
||||
with warnings.catch_warnings(record=True):
|
||||
target[assigner] = ret
|
||||
except (TypeError, IndexError):
|
||||
raise ValueError("Cannot assign expression output to target")
|
||||
|
||||
if not resolvers:
|
||||
resolvers = ({assigner: ret},)
|
||||
else:
|
||||
# existing resolver needs updated to handle
|
||||
# case of mutating existing column in copy
|
||||
for resolver in resolvers:
|
||||
if assigner in resolver:
|
||||
resolver[assigner] = ret
|
||||
break
|
||||
else:
|
||||
resolvers += ({assigner: ret},)
|
||||
|
||||
ret = None
|
||||
first_expr = False
|
||||
|
||||
# We want to exclude `inplace=None` as being False.
|
||||
if inplace is False:
|
||||
return target if target_modified else ret
|
||||
@@ -0,0 +1,766 @@
|
||||
""":func:`~pandas.eval` parsers
|
||||
"""
|
||||
|
||||
import ast
|
||||
import tokenize
|
||||
|
||||
from functools import partial
|
||||
import numpy as np
|
||||
|
||||
import pandas as pd
|
||||
from pandas import compat
|
||||
from pandas.compat import StringIO, lmap, zip, reduce, string_types
|
||||
from pandas.core.base import StringMixin
|
||||
from pandas.core import common as com
|
||||
import pandas.io.formats.printing as printing
|
||||
from pandas.core.reshape.util import compose
|
||||
from pandas.core.computation.ops import (
|
||||
_cmp_ops_syms, _bool_ops_syms,
|
||||
_arith_ops_syms, _unary_ops_syms, is_term)
|
||||
from pandas.core.computation.ops import _reductions, _mathops, _LOCAL_TAG
|
||||
from pandas.core.computation.ops import Op, BinOp, UnaryOp, Term, Constant, Div
|
||||
from pandas.core.computation.ops import UndefinedVariableError, FuncNode
|
||||
from pandas.core.computation.scope import Scope
|
||||
|
||||
|
||||
def tokenize_string(source):
|
||||
"""Tokenize a Python source code string.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
source : str
|
||||
A Python source code string
|
||||
"""
|
||||
line_reader = StringIO(source).readline
|
||||
for toknum, tokval, _, _, _ in tokenize.generate_tokens(line_reader):
|
||||
yield toknum, tokval
|
||||
|
||||
|
||||
def _rewrite_assign(tok):
|
||||
"""Rewrite the assignment operator for PyTables expressions that use ``=``
|
||||
as a substitute for ``==``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tok : tuple of int, str
|
||||
ints correspond to the all caps constants in the tokenize module
|
||||
|
||||
Returns
|
||||
-------
|
||||
t : tuple of int, str
|
||||
Either the input or token or the replacement values
|
||||
"""
|
||||
toknum, tokval = tok
|
||||
return toknum, '==' if tokval == '=' else tokval
|
||||
|
||||
|
||||
def _replace_booleans(tok):
|
||||
"""Replace ``&`` with ``and`` and ``|`` with ``or`` so that bitwise
|
||||
precedence is changed to boolean precedence.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tok : tuple of int, str
|
||||
ints correspond to the all caps constants in the tokenize module
|
||||
|
||||
Returns
|
||||
-------
|
||||
t : tuple of int, str
|
||||
Either the input or token or the replacement values
|
||||
"""
|
||||
toknum, tokval = tok
|
||||
if toknum == tokenize.OP:
|
||||
if tokval == '&':
|
||||
return tokenize.NAME, 'and'
|
||||
elif tokval == '|':
|
||||
return tokenize.NAME, 'or'
|
||||
return toknum, tokval
|
||||
return toknum, tokval
|
||||
|
||||
|
||||
def _replace_locals(tok):
|
||||
"""Replace local variables with a syntactically valid name.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tok : tuple of int, str
|
||||
ints correspond to the all caps constants in the tokenize module
|
||||
|
||||
Returns
|
||||
-------
|
||||
t : tuple of int, str
|
||||
Either the input or token or the replacement values
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is somewhat of a hack in that we rewrite a string such as ``'@a'`` as
|
||||
``'__pd_eval_local_a'`` by telling the tokenizer that ``__pd_eval_local_``
|
||||
is a ``tokenize.OP`` and to replace the ``'@'`` symbol with it.
|
||||
"""
|
||||
toknum, tokval = tok
|
||||
if toknum == tokenize.OP and tokval == '@':
|
||||
return tokenize.OP, _LOCAL_TAG
|
||||
return toknum, tokval
|
||||
|
||||
|
||||
def _preparse(source, f=compose(_replace_locals, _replace_booleans,
|
||||
_rewrite_assign)):
|
||||
"""Compose a collection of tokenization functions
|
||||
|
||||
Parameters
|
||||
----------
|
||||
source : str
|
||||
A Python source code string
|
||||
f : callable
|
||||
This takes a tuple of (toknum, tokval) as its argument and returns a
|
||||
tuple with the same structure but possibly different elements. Defaults
|
||||
to the composition of ``_rewrite_assign``, ``_replace_booleans``, and
|
||||
``_replace_locals``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
s : str
|
||||
Valid Python source code
|
||||
|
||||
Notes
|
||||
-----
|
||||
The `f` parameter can be any callable that takes *and* returns input of the
|
||||
form ``(toknum, tokval)``, where ``toknum`` is one of the constants from
|
||||
the ``tokenize`` module and ``tokval`` is a string.
|
||||
"""
|
||||
assert callable(f), 'f must be callable'
|
||||
return tokenize.untokenize(lmap(f, tokenize_string(source)))
|
||||
|
||||
|
||||
def _is_type(t):
|
||||
"""Factory for a type checking function of type ``t`` or tuple of types."""
|
||||
return lambda x: isinstance(x.value, t)
|
||||
|
||||
|
||||
_is_list = _is_type(list)
|
||||
_is_str = _is_type(string_types)
|
||||
|
||||
|
||||
# partition all AST nodes
|
||||
_all_nodes = frozenset(filter(lambda x: isinstance(x, type) and
|
||||
issubclass(x, ast.AST),
|
||||
(getattr(ast, node) for node in dir(ast))))
|
||||
|
||||
|
||||
def _filter_nodes(superclass, all_nodes=_all_nodes):
|
||||
"""Filter out AST nodes that are subclasses of ``superclass``."""
|
||||
node_names = (node.__name__ for node in all_nodes
|
||||
if issubclass(node, superclass))
|
||||
return frozenset(node_names)
|
||||
|
||||
|
||||
_all_node_names = frozenset(map(lambda x: x.__name__, _all_nodes))
|
||||
_mod_nodes = _filter_nodes(ast.mod)
|
||||
_stmt_nodes = _filter_nodes(ast.stmt)
|
||||
_expr_nodes = _filter_nodes(ast.expr)
|
||||
_expr_context_nodes = _filter_nodes(ast.expr_context)
|
||||
_slice_nodes = _filter_nodes(ast.slice)
|
||||
_boolop_nodes = _filter_nodes(ast.boolop)
|
||||
_operator_nodes = _filter_nodes(ast.operator)
|
||||
_unary_op_nodes = _filter_nodes(ast.unaryop)
|
||||
_cmp_op_nodes = _filter_nodes(ast.cmpop)
|
||||
_comprehension_nodes = _filter_nodes(ast.comprehension)
|
||||
_handler_nodes = _filter_nodes(ast.excepthandler)
|
||||
_arguments_nodes = _filter_nodes(ast.arguments)
|
||||
_keyword_nodes = _filter_nodes(ast.keyword)
|
||||
_alias_nodes = _filter_nodes(ast.alias)
|
||||
|
||||
|
||||
# nodes that we don't support directly but are needed for parsing
|
||||
_hacked_nodes = frozenset(['Assign', 'Module', 'Expr'])
|
||||
|
||||
|
||||
_unsupported_expr_nodes = frozenset(['Yield', 'GeneratorExp', 'IfExp',
|
||||
'DictComp', 'SetComp', 'Repr', 'Lambda',
|
||||
'Set', 'AST', 'Is', 'IsNot'])
|
||||
|
||||
# these nodes are low priority or won't ever be supported (e.g., AST)
|
||||
_unsupported_nodes = ((_stmt_nodes | _mod_nodes | _handler_nodes |
|
||||
_arguments_nodes | _keyword_nodes | _alias_nodes |
|
||||
_expr_context_nodes | _unsupported_expr_nodes) -
|
||||
_hacked_nodes)
|
||||
|
||||
# we're adding a different assignment in some cases to be equality comparison
|
||||
# and we don't want `stmt` and friends in their so get only the class whose
|
||||
# names are capitalized
|
||||
_base_supported_nodes = (_all_node_names - _unsupported_nodes) | _hacked_nodes
|
||||
_msg = 'cannot both support and not support {intersection}'.format(
|
||||
intersection=_unsupported_nodes & _base_supported_nodes)
|
||||
assert not _unsupported_nodes & _base_supported_nodes, _msg
|
||||
|
||||
|
||||
def _node_not_implemented(node_name, cls):
|
||||
"""Return a function that raises a NotImplementedError with a passed node
|
||||
name.
|
||||
"""
|
||||
|
||||
def f(self, *args, **kwargs):
|
||||
raise NotImplementedError("{name!r} nodes are not "
|
||||
"implemented".format(name=node_name))
|
||||
return f
|
||||
|
||||
|
||||
def disallow(nodes):
|
||||
"""Decorator to disallow certain nodes from parsing. Raises a
|
||||
NotImplementedError instead.
|
||||
|
||||
Returns
|
||||
-------
|
||||
disallowed : callable
|
||||
"""
|
||||
def disallowed(cls):
|
||||
cls.unsupported_nodes = ()
|
||||
for node in nodes:
|
||||
new_method = _node_not_implemented(node, cls)
|
||||
name = 'visit_{node}'.format(node=node)
|
||||
cls.unsupported_nodes += (name,)
|
||||
setattr(cls, name, new_method)
|
||||
return cls
|
||||
return disallowed
|
||||
|
||||
|
||||
def _op_maker(op_class, op_symbol):
|
||||
"""Return a function to create an op class with its symbol already passed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
f : callable
|
||||
"""
|
||||
|
||||
def f(self, node, *args, **kwargs):
|
||||
"""Return a partial function with an Op subclass with an operator
|
||||
already passed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
f : callable
|
||||
"""
|
||||
return partial(op_class, op_symbol, *args, **kwargs)
|
||||
return f
|
||||
|
||||
|
||||
_op_classes = {'binary': BinOp, 'unary': UnaryOp}
|
||||
|
||||
|
||||
def add_ops(op_classes):
|
||||
"""Decorator to add default implementation of ops."""
|
||||
def f(cls):
|
||||
for op_attr_name, op_class in compat.iteritems(op_classes):
|
||||
ops = getattr(cls, '{name}_ops'.format(name=op_attr_name))
|
||||
ops_map = getattr(cls, '{name}_op_nodes_map'.format(
|
||||
name=op_attr_name))
|
||||
for op in ops:
|
||||
op_node = ops_map[op]
|
||||
if op_node is not None:
|
||||
made_op = _op_maker(op_class, op)
|
||||
setattr(cls, 'visit_{node}'.format(node=op_node), made_op)
|
||||
return cls
|
||||
return f
|
||||
|
||||
|
||||
@disallow(_unsupported_nodes)
|
||||
@add_ops(_op_classes)
|
||||
class BaseExprVisitor(ast.NodeVisitor):
|
||||
|
||||
"""Custom ast walker. Parsers of other engines should subclass this class
|
||||
if necessary.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
env : Scope
|
||||
engine : str
|
||||
parser : str
|
||||
preparser : callable
|
||||
"""
|
||||
const_type = Constant
|
||||
term_type = Term
|
||||
|
||||
binary_ops = _cmp_ops_syms + _bool_ops_syms + _arith_ops_syms
|
||||
binary_op_nodes = ('Gt', 'Lt', 'GtE', 'LtE', 'Eq', 'NotEq', 'In', 'NotIn',
|
||||
'BitAnd', 'BitOr', 'And', 'Or', 'Add', 'Sub', 'Mult',
|
||||
None, 'Pow', 'FloorDiv', 'Mod')
|
||||
binary_op_nodes_map = dict(zip(binary_ops, binary_op_nodes))
|
||||
|
||||
unary_ops = _unary_ops_syms
|
||||
unary_op_nodes = 'UAdd', 'USub', 'Invert', 'Not'
|
||||
unary_op_nodes_map = dict(zip(unary_ops, unary_op_nodes))
|
||||
|
||||
rewrite_map = {
|
||||
ast.Eq: ast.In,
|
||||
ast.NotEq: ast.NotIn,
|
||||
ast.In: ast.In,
|
||||
ast.NotIn: ast.NotIn
|
||||
}
|
||||
|
||||
def __init__(self, env, engine, parser, preparser=_preparse):
|
||||
self.env = env
|
||||
self.engine = engine
|
||||
self.parser = parser
|
||||
self.preparser = preparser
|
||||
self.assigner = None
|
||||
|
||||
def visit(self, node, **kwargs):
|
||||
if isinstance(node, string_types):
|
||||
clean = self.preparser(node)
|
||||
try:
|
||||
node = ast.fix_missing_locations(ast.parse(clean))
|
||||
except SyntaxError as e:
|
||||
from keyword import iskeyword
|
||||
if any(iskeyword(x) for x in clean.split()):
|
||||
e.msg = ("Python keyword not valid identifier"
|
||||
" in numexpr query")
|
||||
raise e
|
||||
|
||||
method = 'visit_' + node.__class__.__name__
|
||||
visitor = getattr(self, method)
|
||||
return visitor(node, **kwargs)
|
||||
|
||||
def visit_Module(self, node, **kwargs):
|
||||
if len(node.body) != 1:
|
||||
raise SyntaxError('only a single expression is allowed')
|
||||
expr = node.body[0]
|
||||
return self.visit(expr, **kwargs)
|
||||
|
||||
def visit_Expr(self, node, **kwargs):
|
||||
return self.visit(node.value, **kwargs)
|
||||
|
||||
def _rewrite_membership_op(self, node, left, right):
|
||||
# the kind of the operator (is actually an instance)
|
||||
op_instance = node.op
|
||||
op_type = type(op_instance)
|
||||
|
||||
# must be two terms and the comparison operator must be ==/!=/in/not in
|
||||
if is_term(left) and is_term(right) and op_type in self.rewrite_map:
|
||||
|
||||
left_list, right_list = map(_is_list, (left, right))
|
||||
left_str, right_str = map(_is_str, (left, right))
|
||||
|
||||
# if there are any strings or lists in the expression
|
||||
if left_list or right_list or left_str or right_str:
|
||||
op_instance = self.rewrite_map[op_type]()
|
||||
|
||||
# pop the string variable out of locals and replace it with a list
|
||||
# of one string, kind of a hack
|
||||
if right_str:
|
||||
name = self.env.add_tmp([right.value])
|
||||
right = self.term_type(name, self.env)
|
||||
|
||||
if left_str:
|
||||
name = self.env.add_tmp([left.value])
|
||||
left = self.term_type(name, self.env)
|
||||
|
||||
op = self.visit(op_instance)
|
||||
return op, op_instance, left, right
|
||||
|
||||
def _maybe_transform_eq_ne(self, node, left=None, right=None):
|
||||
if left is None:
|
||||
left = self.visit(node.left, side='left')
|
||||
if right is None:
|
||||
right = self.visit(node.right, side='right')
|
||||
op, op_class, left, right = self._rewrite_membership_op(node, left,
|
||||
right)
|
||||
return op, op_class, left, right
|
||||
|
||||
def _maybe_downcast_constants(self, left, right):
|
||||
f32 = np.dtype(np.float32)
|
||||
if left.is_scalar and not right.is_scalar and right.return_type == f32:
|
||||
# right is a float32 array, left is a scalar
|
||||
name = self.env.add_tmp(np.float32(left.value))
|
||||
left = self.term_type(name, self.env)
|
||||
if right.is_scalar and not left.is_scalar and left.return_type == f32:
|
||||
# left is a float32 array, right is a scalar
|
||||
name = self.env.add_tmp(np.float32(right.value))
|
||||
right = self.term_type(name, self.env)
|
||||
|
||||
return left, right
|
||||
|
||||
def _maybe_eval(self, binop, eval_in_python):
|
||||
# eval `in` and `not in` (for now) in "partial" python space
|
||||
# things that can be evaluated in "eval" space will be turned into
|
||||
# temporary variables. for example,
|
||||
# [1,2] in a + 2 * b
|
||||
# in that case a + 2 * b will be evaluated using numexpr, and the "in"
|
||||
# call will be evaluated using isin (in python space)
|
||||
return binop.evaluate(self.env, self.engine, self.parser,
|
||||
self.term_type, eval_in_python)
|
||||
|
||||
def _maybe_evaluate_binop(self, op, op_class, lhs, rhs,
|
||||
eval_in_python=('in', 'not in'),
|
||||
maybe_eval_in_python=('==', '!=', '<', '>',
|
||||
'<=', '>=')):
|
||||
res = op(lhs, rhs)
|
||||
|
||||
if res.has_invalid_return_type:
|
||||
raise TypeError("unsupported operand type(s) for {op}:"
|
||||
" '{lhs}' and '{rhs}'".format(op=res.op,
|
||||
lhs=lhs.type,
|
||||
rhs=rhs.type))
|
||||
|
||||
if self.engine != 'pytables':
|
||||
if (res.op in _cmp_ops_syms and
|
||||
getattr(lhs, 'is_datetime', False) or
|
||||
getattr(rhs, 'is_datetime', False)):
|
||||
# all date ops must be done in python bc numexpr doesn't work
|
||||
# well with NaT
|
||||
return self._maybe_eval(res, self.binary_ops)
|
||||
|
||||
if res.op in eval_in_python:
|
||||
# "in"/"not in" ops are always evaluated in python
|
||||
return self._maybe_eval(res, eval_in_python)
|
||||
elif self.engine != 'pytables':
|
||||
if (getattr(lhs, 'return_type', None) == object or
|
||||
getattr(rhs, 'return_type', None) == object):
|
||||
# evaluate "==" and "!=" in python if either of our operands
|
||||
# has an object return type
|
||||
return self._maybe_eval(res, eval_in_python +
|
||||
maybe_eval_in_python)
|
||||
return res
|
||||
|
||||
def visit_BinOp(self, node, **kwargs):
|
||||
op, op_class, left, right = self._maybe_transform_eq_ne(node)
|
||||
left, right = self._maybe_downcast_constants(left, right)
|
||||
return self._maybe_evaluate_binop(op, op_class, left, right)
|
||||
|
||||
def visit_Div(self, node, **kwargs):
|
||||
truediv = self.env.scope['truediv']
|
||||
return lambda lhs, rhs: Div(lhs, rhs, truediv)
|
||||
|
||||
def visit_UnaryOp(self, node, **kwargs):
|
||||
op = self.visit(node.op)
|
||||
operand = self.visit(node.operand)
|
||||
return op(operand)
|
||||
|
||||
def visit_Name(self, node, **kwargs):
|
||||
return self.term_type(node.id, self.env, **kwargs)
|
||||
|
||||
def visit_NameConstant(self, node, **kwargs):
|
||||
return self.const_type(node.value, self.env)
|
||||
|
||||
def visit_Num(self, node, **kwargs):
|
||||
return self.const_type(node.n, self.env)
|
||||
|
||||
def visit_Str(self, node, **kwargs):
|
||||
name = self.env.add_tmp(node.s)
|
||||
return self.term_type(name, self.env)
|
||||
|
||||
def visit_List(self, node, **kwargs):
|
||||
name = self.env.add_tmp([self.visit(e)(self.env) for e in node.elts])
|
||||
return self.term_type(name, self.env)
|
||||
|
||||
visit_Tuple = visit_List
|
||||
|
||||
def visit_Index(self, node, **kwargs):
|
||||
""" df.index[4] """
|
||||
return self.visit(node.value)
|
||||
|
||||
def visit_Subscript(self, node, **kwargs):
|
||||
value = self.visit(node.value)
|
||||
slobj = self.visit(node.slice)
|
||||
result = pd.eval(slobj, local_dict=self.env, engine=self.engine,
|
||||
parser=self.parser)
|
||||
try:
|
||||
# a Term instance
|
||||
v = value.value[result]
|
||||
except AttributeError:
|
||||
# an Op instance
|
||||
lhs = pd.eval(value, local_dict=self.env, engine=self.engine,
|
||||
parser=self.parser)
|
||||
v = lhs[result]
|
||||
name = self.env.add_tmp(v)
|
||||
return self.term_type(name, env=self.env)
|
||||
|
||||
def visit_Slice(self, node, **kwargs):
|
||||
""" df.index[slice(4,6)] """
|
||||
lower = node.lower
|
||||
if lower is not None:
|
||||
lower = self.visit(lower).value
|
||||
upper = node.upper
|
||||
if upper is not None:
|
||||
upper = self.visit(upper).value
|
||||
step = node.step
|
||||
if step is not None:
|
||||
step = self.visit(step).value
|
||||
|
||||
return slice(lower, upper, step)
|
||||
|
||||
def visit_Assign(self, node, **kwargs):
|
||||
"""
|
||||
support a single assignment node, like
|
||||
|
||||
c = a + b
|
||||
|
||||
set the assigner at the top level, must be a Name node which
|
||||
might or might not exist in the resolvers
|
||||
|
||||
"""
|
||||
|
||||
if len(node.targets) != 1:
|
||||
raise SyntaxError('can only assign a single expression')
|
||||
if not isinstance(node.targets[0], ast.Name):
|
||||
raise SyntaxError('left hand side of an assignment must be a '
|
||||
'single name')
|
||||
if self.env.target is None:
|
||||
raise ValueError('cannot assign without a target object')
|
||||
|
||||
try:
|
||||
assigner = self.visit(node.targets[0], **kwargs)
|
||||
except UndefinedVariableError:
|
||||
assigner = node.targets[0].id
|
||||
|
||||
self.assigner = getattr(assigner, 'name', assigner)
|
||||
if self.assigner is None:
|
||||
raise SyntaxError('left hand side of an assignment must be a '
|
||||
'single resolvable name')
|
||||
|
||||
return self.visit(node.value, **kwargs)
|
||||
|
||||
def visit_Attribute(self, node, **kwargs):
|
||||
attr = node.attr
|
||||
value = node.value
|
||||
|
||||
ctx = node.ctx
|
||||
if isinstance(ctx, ast.Load):
|
||||
# resolve the value
|
||||
resolved = self.visit(value).value
|
||||
try:
|
||||
v = getattr(resolved, attr)
|
||||
name = self.env.add_tmp(v)
|
||||
return self.term_type(name, self.env)
|
||||
except AttributeError:
|
||||
# something like datetime.datetime where scope is overridden
|
||||
if isinstance(value, ast.Name) and value.id == attr:
|
||||
return resolved
|
||||
|
||||
raise ValueError("Invalid Attribute context {name}"
|
||||
.format(name=ctx.__name__))
|
||||
|
||||
def visit_Call_35(self, node, side=None, **kwargs):
|
||||
""" in 3.5 the starargs attribute was changed to be more flexible,
|
||||
#11097 """
|
||||
|
||||
if isinstance(node.func, ast.Attribute):
|
||||
res = self.visit_Attribute(node.func)
|
||||
elif not isinstance(node.func, ast.Name):
|
||||
raise TypeError("Only named functions are supported")
|
||||
else:
|
||||
try:
|
||||
res = self.visit(node.func)
|
||||
except UndefinedVariableError:
|
||||
# Check if this is a supported function name
|
||||
try:
|
||||
res = FuncNode(node.func.id)
|
||||
except ValueError:
|
||||
# Raise original error
|
||||
raise
|
||||
|
||||
if res is None:
|
||||
raise ValueError("Invalid function call {func}"
|
||||
.format(func=node.func.id))
|
||||
if hasattr(res, 'value'):
|
||||
res = res.value
|
||||
|
||||
if isinstance(res, FuncNode):
|
||||
|
||||
new_args = [self.visit(arg) for arg in node.args]
|
||||
|
||||
if node.keywords:
|
||||
raise TypeError("Function \"{name}\" does not support keyword "
|
||||
"arguments".format(name=res.name))
|
||||
|
||||
return res(*new_args, **kwargs)
|
||||
|
||||
else:
|
||||
|
||||
new_args = [self.visit(arg).value for arg in node.args]
|
||||
|
||||
for key in node.keywords:
|
||||
if not isinstance(key, ast.keyword):
|
||||
raise ValueError("keyword error in function call "
|
||||
"'{func}'".format(func=node.func.id))
|
||||
|
||||
if key.arg:
|
||||
# TODO: bug?
|
||||
kwargs.append(ast.keyword(
|
||||
keyword.arg, self.visit(keyword.value))) # noqa
|
||||
|
||||
return self.const_type(res(*new_args, **kwargs), self.env)
|
||||
|
||||
def visit_Call_legacy(self, node, side=None, **kwargs):
|
||||
|
||||
# this can happen with: datetime.datetime
|
||||
if isinstance(node.func, ast.Attribute):
|
||||
res = self.visit_Attribute(node.func)
|
||||
elif not isinstance(node.func, ast.Name):
|
||||
raise TypeError("Only named functions are supported")
|
||||
else:
|
||||
try:
|
||||
res = self.visit(node.func)
|
||||
except UndefinedVariableError:
|
||||
# Check if this is a supported function name
|
||||
try:
|
||||
res = FuncNode(node.func.id)
|
||||
except ValueError:
|
||||
# Raise original error
|
||||
raise
|
||||
|
||||
if res is None:
|
||||
raise ValueError("Invalid function call {func}"
|
||||
.format(func=node.func.id))
|
||||
if hasattr(res, 'value'):
|
||||
res = res.value
|
||||
|
||||
if isinstance(res, FuncNode):
|
||||
args = [self.visit(targ) for targ in node.args]
|
||||
|
||||
if node.starargs is not None:
|
||||
args += self.visit(node.starargs)
|
||||
|
||||
if node.keywords or node.kwargs:
|
||||
raise TypeError("Function \"{name}\" does not support keyword "
|
||||
"arguments".format(name=res.name))
|
||||
|
||||
return res(*args, **kwargs)
|
||||
|
||||
else:
|
||||
args = [self.visit(targ).value for targ in node.args]
|
||||
if node.starargs is not None:
|
||||
args += self.visit(node.starargs).value
|
||||
|
||||
keywords = {}
|
||||
for key in node.keywords:
|
||||
if not isinstance(key, ast.keyword):
|
||||
raise ValueError("keyword error in function call "
|
||||
"'{func}'".format(func=node.func.id))
|
||||
keywords[key.arg] = self.visit(key.value).value
|
||||
if node.kwargs is not None:
|
||||
keywords.update(self.visit(node.kwargs).value)
|
||||
|
||||
return self.const_type(res(*args, **keywords), self.env)
|
||||
|
||||
def translate_In(self, op):
|
||||
return op
|
||||
|
||||
def visit_Compare(self, node, **kwargs):
|
||||
ops = node.ops
|
||||
comps = node.comparators
|
||||
|
||||
# base case: we have something like a CMP b
|
||||
if len(comps) == 1:
|
||||
op = self.translate_In(ops[0])
|
||||
binop = ast.BinOp(op=op, left=node.left, right=comps[0])
|
||||
return self.visit(binop)
|
||||
|
||||
# recursive case: we have a chained comparison, a CMP b CMP c, etc.
|
||||
left = node.left
|
||||
values = []
|
||||
for op, comp in zip(ops, comps):
|
||||
new_node = self.visit(ast.Compare(comparators=[comp], left=left,
|
||||
ops=[self.translate_In(op)]))
|
||||
left = comp
|
||||
values.append(new_node)
|
||||
return self.visit(ast.BoolOp(op=ast.And(), values=values))
|
||||
|
||||
def _try_visit_binop(self, bop):
|
||||
if isinstance(bop, (Op, Term)):
|
||||
return bop
|
||||
return self.visit(bop)
|
||||
|
||||
def visit_BoolOp(self, node, **kwargs):
|
||||
def visitor(x, y):
|
||||
lhs = self._try_visit_binop(x)
|
||||
rhs = self._try_visit_binop(y)
|
||||
|
||||
op, op_class, lhs, rhs = self._maybe_transform_eq_ne(
|
||||
node, lhs, rhs)
|
||||
return self._maybe_evaluate_binop(op, node.op, lhs, rhs)
|
||||
|
||||
operands = node.values
|
||||
return reduce(visitor, operands)
|
||||
|
||||
|
||||
# ast.Call signature changed on 3.5,
|
||||
# conditionally change which methods is named
|
||||
# visit_Call depending on Python version, #11097
|
||||
if compat.PY35:
|
||||
BaseExprVisitor.visit_Call = BaseExprVisitor.visit_Call_35
|
||||
else:
|
||||
BaseExprVisitor.visit_Call = BaseExprVisitor.visit_Call_legacy
|
||||
|
||||
_python_not_supported = frozenset(['Dict', 'BoolOp', 'In', 'NotIn'])
|
||||
_numexpr_supported_calls = frozenset(_reductions + _mathops)
|
||||
|
||||
|
||||
@disallow((_unsupported_nodes | _python_not_supported) -
|
||||
(_boolop_nodes | frozenset(['BoolOp', 'Attribute', 'In', 'NotIn',
|
||||
'Tuple'])))
|
||||
class PandasExprVisitor(BaseExprVisitor):
|
||||
|
||||
def __init__(self, env, engine, parser,
|
||||
preparser=partial(_preparse, f=compose(_replace_locals,
|
||||
_replace_booleans))):
|
||||
super(PandasExprVisitor, self).__init__(env, engine, parser, preparser)
|
||||
|
||||
|
||||
@disallow(_unsupported_nodes | _python_not_supported | frozenset(['Not']))
|
||||
class PythonExprVisitor(BaseExprVisitor):
|
||||
|
||||
def __init__(self, env, engine, parser, preparser=lambda x: x):
|
||||
super(PythonExprVisitor, self).__init__(env, engine, parser,
|
||||
preparser=preparser)
|
||||
|
||||
|
||||
class Expr(StringMixin):
|
||||
|
||||
"""Object encapsulating an expression.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
expr : str
|
||||
engine : str, optional, default 'numexpr'
|
||||
parser : str, optional, default 'pandas'
|
||||
env : Scope, optional, default None
|
||||
truediv : bool, optional, default True
|
||||
level : int, optional, default 2
|
||||
"""
|
||||
|
||||
def __init__(self, expr, engine='numexpr', parser='pandas', env=None,
|
||||
truediv=True, level=0):
|
||||
self.expr = expr
|
||||
self.env = env or Scope(level=level + 1)
|
||||
self.engine = engine
|
||||
self.parser = parser
|
||||
self.env.scope['truediv'] = truediv
|
||||
self._visitor = _parsers[parser](self.env, self.engine, self.parser)
|
||||
self.terms = self.parse()
|
||||
|
||||
@property
|
||||
def assigner(self):
|
||||
return getattr(self._visitor, 'assigner', None)
|
||||
|
||||
def __call__(self):
|
||||
return self.terms(self.env)
|
||||
|
||||
def __unicode__(self):
|
||||
return printing.pprint_thing(self.terms)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.expr)
|
||||
|
||||
def parse(self):
|
||||
"""Parse an expression"""
|
||||
return self._visitor.visit(self.expr)
|
||||
|
||||
@property
|
||||
def names(self):
|
||||
"""Get the names in an expression"""
|
||||
if is_term(self.terms):
|
||||
return frozenset([self.terms.name])
|
||||
return frozenset(term.name for term in com.flatten(self.terms))
|
||||
|
||||
|
||||
_parsers = {'python': PythonExprVisitor, 'pandas': PandasExprVisitor}
|
||||
@@ -0,0 +1,248 @@
|
||||
"""
|
||||
Expressions
|
||||
-----------
|
||||
|
||||
Offer fast expression evaluation through numexpr
|
||||
|
||||
"""
|
||||
|
||||
import warnings
|
||||
import numpy as np
|
||||
|
||||
import pandas.core.common as com
|
||||
from pandas.core.computation.check import _NUMEXPR_INSTALLED
|
||||
from pandas.core.config import get_option
|
||||
|
||||
if _NUMEXPR_INSTALLED:
|
||||
import numexpr as ne
|
||||
|
||||
_TEST_MODE = None
|
||||
_TEST_RESULT = None
|
||||
_USE_NUMEXPR = _NUMEXPR_INSTALLED
|
||||
_evaluate = None
|
||||
_where = None
|
||||
|
||||
# the set of dtypes that we will allow pass to numexpr
|
||||
_ALLOWED_DTYPES = {
|
||||
'evaluate': set(['int64', 'int32', 'float64', 'float32', 'bool']),
|
||||
'where': set(['int64', 'float64', 'bool'])
|
||||
}
|
||||
|
||||
# the minimum prod shape that we will use numexpr
|
||||
_MIN_ELEMENTS = 10000
|
||||
|
||||
|
||||
def set_use_numexpr(v=True):
|
||||
# set/unset to use numexpr
|
||||
global _USE_NUMEXPR
|
||||
if _NUMEXPR_INSTALLED:
|
||||
_USE_NUMEXPR = v
|
||||
|
||||
# choose what we are going to do
|
||||
global _evaluate, _where
|
||||
if not _USE_NUMEXPR:
|
||||
_evaluate = _evaluate_standard
|
||||
_where = _where_standard
|
||||
else:
|
||||
_evaluate = _evaluate_numexpr
|
||||
_where = _where_numexpr
|
||||
|
||||
|
||||
def set_numexpr_threads(n=None):
|
||||
# if we are using numexpr, set the threads to n
|
||||
# otherwise reset
|
||||
if _NUMEXPR_INSTALLED and _USE_NUMEXPR:
|
||||
if n is None:
|
||||
n = ne.detect_number_of_cores()
|
||||
ne.set_num_threads(n)
|
||||
|
||||
|
||||
def _evaluate_standard(op, op_str, a, b, **eval_kwargs):
|
||||
""" standard evaluation """
|
||||
if _TEST_MODE:
|
||||
_store_test_result(False)
|
||||
with np.errstate(all='ignore'):
|
||||
return op(a, b)
|
||||
|
||||
|
||||
def _can_use_numexpr(op, op_str, a, b, dtype_check):
|
||||
""" return a boolean if we WILL be using numexpr """
|
||||
if op_str is not None:
|
||||
|
||||
# required min elements (otherwise we are adding overhead)
|
||||
if np.prod(a.shape) > _MIN_ELEMENTS:
|
||||
|
||||
# check for dtype compatibility
|
||||
dtypes = set()
|
||||
for o in [a, b]:
|
||||
if hasattr(o, 'get_dtype_counts'):
|
||||
s = o.get_dtype_counts()
|
||||
if len(s) > 1:
|
||||
return False
|
||||
dtypes |= set(s.index)
|
||||
elif isinstance(o, np.ndarray):
|
||||
dtypes |= set([o.dtype.name])
|
||||
|
||||
# allowed are a superset
|
||||
if not len(dtypes) or _ALLOWED_DTYPES[dtype_check] >= dtypes:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _evaluate_numexpr(op, op_str, a, b, truediv=True,
|
||||
reversed=False, **eval_kwargs):
|
||||
result = None
|
||||
|
||||
if _can_use_numexpr(op, op_str, a, b, 'evaluate'):
|
||||
try:
|
||||
|
||||
# we were originally called by a reversed op
|
||||
# method
|
||||
if reversed:
|
||||
a, b = b, a
|
||||
|
||||
a_value = getattr(a, "values", a)
|
||||
b_value = getattr(b, "values", b)
|
||||
result = ne.evaluate('a_value {op} b_value'.format(op=op_str),
|
||||
local_dict={'a_value': a_value,
|
||||
'b_value': b_value},
|
||||
casting='safe', truediv=truediv,
|
||||
**eval_kwargs)
|
||||
except ValueError as detail:
|
||||
if 'unknown type object' in str(detail):
|
||||
pass
|
||||
|
||||
if _TEST_MODE:
|
||||
_store_test_result(result is not None)
|
||||
|
||||
if result is None:
|
||||
result = _evaluate_standard(op, op_str, a, b)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _where_standard(cond, a, b):
|
||||
return np.where(com._values_from_object(cond), com._values_from_object(a),
|
||||
com._values_from_object(b))
|
||||
|
||||
|
||||
def _where_numexpr(cond, a, b):
|
||||
result = None
|
||||
|
||||
if _can_use_numexpr(None, 'where', a, b, 'where'):
|
||||
|
||||
try:
|
||||
cond_value = getattr(cond, 'values', cond)
|
||||
a_value = getattr(a, 'values', a)
|
||||
b_value = getattr(b, 'values', b)
|
||||
result = ne.evaluate('where(cond_value, a_value, b_value)',
|
||||
local_dict={'cond_value': cond_value,
|
||||
'a_value': a_value,
|
||||
'b_value': b_value},
|
||||
casting='safe')
|
||||
except ValueError as detail:
|
||||
if 'unknown type object' in str(detail):
|
||||
pass
|
||||
except Exception as detail:
|
||||
raise TypeError(str(detail))
|
||||
|
||||
if result is None:
|
||||
result = _where_standard(cond, a, b)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# turn myself on
|
||||
set_use_numexpr(get_option('compute.use_numexpr'))
|
||||
|
||||
|
||||
def _has_bool_dtype(x):
|
||||
try:
|
||||
return x.dtype == bool
|
||||
except AttributeError:
|
||||
try:
|
||||
return 'bool' in x.dtypes
|
||||
except AttributeError:
|
||||
return isinstance(x, (bool, np.bool_))
|
||||
|
||||
|
||||
def _bool_arith_check(op_str, a, b, not_allowed=frozenset(('/', '//', '**')),
|
||||
unsupported=None):
|
||||
if unsupported is None:
|
||||
unsupported = {'+': '|', '*': '&', '-': '^'}
|
||||
|
||||
if _has_bool_dtype(a) and _has_bool_dtype(b):
|
||||
if op_str in unsupported:
|
||||
warnings.warn("evaluating in Python space because the {op!r} "
|
||||
"operator is not supported by numexpr for "
|
||||
"the bool dtype, use {alt_op!r} instead"
|
||||
.format(op=op_str, alt_op=unsupported[op_str]))
|
||||
return False
|
||||
|
||||
if op_str in not_allowed:
|
||||
raise NotImplementedError("operator {op!r} not implemented for "
|
||||
"bool dtypes".format(op=op_str))
|
||||
return True
|
||||
|
||||
|
||||
def evaluate(op, op_str, a, b, use_numexpr=True,
|
||||
**eval_kwargs):
|
||||
""" evaluate and return the expression of the op on a and b
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
op : the actual operand
|
||||
op_str: the string version of the op
|
||||
a : left operand
|
||||
b : right operand
|
||||
use_numexpr : whether to try to use numexpr (default True)
|
||||
"""
|
||||
|
||||
use_numexpr = use_numexpr and _bool_arith_check(op_str, a, b)
|
||||
if use_numexpr:
|
||||
return _evaluate(op, op_str, a, b, **eval_kwargs)
|
||||
return _evaluate_standard(op, op_str, a, b)
|
||||
|
||||
|
||||
def where(cond, a, b, use_numexpr=True):
|
||||
""" evaluate the where condition cond on a and b
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
cond : a boolean array
|
||||
a : return if cond is True
|
||||
b : return if cond is False
|
||||
use_numexpr : whether to try to use numexpr (default True)
|
||||
"""
|
||||
|
||||
if use_numexpr:
|
||||
return _where(cond, a, b)
|
||||
return _where_standard(cond, a, b)
|
||||
|
||||
|
||||
def set_test_mode(v=True):
|
||||
"""
|
||||
Keeps track of whether numexpr was used. Stores an additional ``True``
|
||||
for every successful use of evaluate with numexpr since the last
|
||||
``get_test_result``
|
||||
"""
|
||||
global _TEST_MODE, _TEST_RESULT
|
||||
_TEST_MODE = v
|
||||
_TEST_RESULT = []
|
||||
|
||||
|
||||
def _store_test_result(used_numexpr):
|
||||
global _TEST_RESULT
|
||||
if used_numexpr:
|
||||
_TEST_RESULT.append(used_numexpr)
|
||||
|
||||
|
||||
def get_test_result():
|
||||
"""get test result and reset test_results"""
|
||||
global _TEST_RESULT
|
||||
res = _TEST_RESULT
|
||||
_TEST_RESULT = []
|
||||
return res
|
||||
@@ -0,0 +1,549 @@
|
||||
"""Operator classes for eval.
|
||||
"""
|
||||
|
||||
import operator as op
|
||||
from functools import partial
|
||||
from datetime import datetime
|
||||
|
||||
import numpy as np
|
||||
|
||||
from pandas.core.dtypes.common import is_list_like, is_scalar
|
||||
import pandas as pd
|
||||
from pandas.compat import PY3, string_types, text_type
|
||||
import pandas.core.common as com
|
||||
from pandas.io.formats.printing import pprint_thing, pprint_thing_encoded
|
||||
from pandas.core.base import StringMixin
|
||||
from pandas.core.computation.common import _ensure_decoded, _result_type_many
|
||||
from pandas.core.computation.scope import _DEFAULT_GLOBALS
|
||||
|
||||
|
||||
_reductions = 'sum', 'prod'
|
||||
|
||||
_unary_math_ops = ('sin', 'cos', 'exp', 'log', 'expm1', 'log1p',
|
||||
'sqrt', 'sinh', 'cosh', 'tanh', 'arcsin', 'arccos',
|
||||
'arctan', 'arccosh', 'arcsinh', 'arctanh', 'abs')
|
||||
_binary_math_ops = ('arctan2',)
|
||||
_mathops = _unary_math_ops + _binary_math_ops
|
||||
|
||||
|
||||
_LOCAL_TAG = '__pd_eval_local_'
|
||||
|
||||
|
||||
class UndefinedVariableError(NameError):
|
||||
|
||||
"""NameError subclass for local variables."""
|
||||
|
||||
def __init__(self, name, is_local):
|
||||
if is_local:
|
||||
msg = 'local variable {0!r} is not defined'
|
||||
else:
|
||||
msg = 'name {0!r} is not defined'
|
||||
super(UndefinedVariableError, self).__init__(msg.format(name))
|
||||
|
||||
|
||||
class Term(StringMixin):
|
||||
|
||||
def __new__(cls, name, env, side=None, encoding=None):
|
||||
klass = Constant if not isinstance(name, string_types) else cls
|
||||
supr_new = super(Term, klass).__new__
|
||||
return supr_new(klass)
|
||||
|
||||
def __init__(self, name, env, side=None, encoding=None):
|
||||
self._name = name
|
||||
self.env = env
|
||||
self.side = side
|
||||
tname = text_type(name)
|
||||
self.is_local = (tname.startswith(_LOCAL_TAG) or
|
||||
tname in _DEFAULT_GLOBALS)
|
||||
self._value = self._resolve_name()
|
||||
self.encoding = encoding
|
||||
|
||||
@property
|
||||
def local_name(self):
|
||||
return self.name.replace(_LOCAL_TAG, '')
|
||||
|
||||
def __unicode__(self):
|
||||
return pprint_thing(self.name)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self.value
|
||||
|
||||
def evaluate(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
def _resolve_name(self):
|
||||
res = self.env.resolve(self.local_name, is_local=self.is_local)
|
||||
self.update(res)
|
||||
|
||||
if hasattr(res, 'ndim') and res.ndim > 2:
|
||||
raise NotImplementedError("N-dimensional objects, where N > 2,"
|
||||
" are not supported with eval")
|
||||
return res
|
||||
|
||||
def update(self, value):
|
||||
"""
|
||||
search order for local (i.e., @variable) variables:
|
||||
|
||||
scope, key_variable
|
||||
[('locals', 'local_name'),
|
||||
('globals', 'local_name'),
|
||||
('locals', 'key'),
|
||||
('globals', 'key')]
|
||||
"""
|
||||
key = self.name
|
||||
|
||||
# if it's a variable name (otherwise a constant)
|
||||
if isinstance(key, string_types):
|
||||
self.env.swapkey(self.local_name, key, new_value=value)
|
||||
|
||||
self.value = value
|
||||
|
||||
@property
|
||||
def is_scalar(self):
|
||||
return is_scalar(self._value)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
try:
|
||||
# potentially very slow for large, mixed dtype frames
|
||||
return self._value.values.dtype
|
||||
except AttributeError:
|
||||
try:
|
||||
# ndarray
|
||||
return self._value.dtype
|
||||
except AttributeError:
|
||||
# scalar
|
||||
return type(self._value)
|
||||
|
||||
return_type = type
|
||||
|
||||
@property
|
||||
def raw(self):
|
||||
return pprint_thing('{0}(name={1!r}, type={2})'
|
||||
''.format(self.__class__.__name__, self.name,
|
||||
self.type))
|
||||
|
||||
@property
|
||||
def is_datetime(self):
|
||||
try:
|
||||
t = self.type.type
|
||||
except AttributeError:
|
||||
t = self.type
|
||||
|
||||
return issubclass(t, (datetime, np.datetime64))
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self._value
|
||||
|
||||
@value.setter
|
||||
def value(self, new_value):
|
||||
self._value = new_value
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, new_name):
|
||||
self._name = new_name
|
||||
|
||||
@property
|
||||
def ndim(self):
|
||||
return self._value.ndim
|
||||
|
||||
|
||||
class Constant(Term):
|
||||
|
||||
def __init__(self, value, env, side=None, encoding=None):
|
||||
super(Constant, self).__init__(value, env, side=side,
|
||||
encoding=encoding)
|
||||
|
||||
def _resolve_name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.value
|
||||
|
||||
def __unicode__(self):
|
||||
# in python 2 str() of float
|
||||
# can truncate shorter than repr()
|
||||
return repr(self.name)
|
||||
|
||||
|
||||
_bool_op_map = {'not': '~', 'and': '&', 'or': '|'}
|
||||
|
||||
|
||||
class Op(StringMixin):
|
||||
|
||||
"""Hold an operator of arbitrary arity
|
||||
"""
|
||||
|
||||
def __init__(self, op, operands, *args, **kwargs):
|
||||
self.op = _bool_op_map.get(op, op)
|
||||
self.operands = operands
|
||||
self.encoding = kwargs.get('encoding', None)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.operands)
|
||||
|
||||
def __unicode__(self):
|
||||
"""Print a generic n-ary operator and its operands using infix
|
||||
notation"""
|
||||
# recurse over the operands
|
||||
parened = ('({0})'.format(pprint_thing(opr))
|
||||
for opr in self.operands)
|
||||
return pprint_thing(' {0} '.format(self.op).join(parened))
|
||||
|
||||
@property
|
||||
def return_type(self):
|
||||
# clobber types to bool if the op is a boolean operator
|
||||
if self.op in (_cmp_ops_syms + _bool_ops_syms):
|
||||
return np.bool_
|
||||
return _result_type_many(*(term.type for term in com.flatten(self)))
|
||||
|
||||
@property
|
||||
def has_invalid_return_type(self):
|
||||
types = self.operand_types
|
||||
obj_dtype_set = frozenset([np.dtype('object')])
|
||||
return self.return_type == object and types - obj_dtype_set
|
||||
|
||||
@property
|
||||
def operand_types(self):
|
||||
return frozenset(term.type for term in com.flatten(self))
|
||||
|
||||
@property
|
||||
def is_scalar(self):
|
||||
return all(operand.is_scalar for operand in self.operands)
|
||||
|
||||
@property
|
||||
def is_datetime(self):
|
||||
try:
|
||||
t = self.return_type.type
|
||||
except AttributeError:
|
||||
t = self.return_type
|
||||
|
||||
return issubclass(t, (datetime, np.datetime64))
|
||||
|
||||
|
||||
def _in(x, y):
|
||||
"""Compute the vectorized membership of ``x in y`` if possible, otherwise
|
||||
use Python.
|
||||
"""
|
||||
try:
|
||||
return x.isin(y)
|
||||
except AttributeError:
|
||||
if is_list_like(x):
|
||||
try:
|
||||
return y.isin(x)
|
||||
except AttributeError:
|
||||
pass
|
||||
return x in y
|
||||
|
||||
|
||||
def _not_in(x, y):
|
||||
"""Compute the vectorized membership of ``x not in y`` if possible,
|
||||
otherwise use Python.
|
||||
"""
|
||||
try:
|
||||
return ~x.isin(y)
|
||||
except AttributeError:
|
||||
if is_list_like(x):
|
||||
try:
|
||||
return ~y.isin(x)
|
||||
except AttributeError:
|
||||
pass
|
||||
return x not in y
|
||||
|
||||
|
||||
_cmp_ops_syms = '>', '<', '>=', '<=', '==', '!=', 'in', 'not in'
|
||||
_cmp_ops_funcs = op.gt, op.lt, op.ge, op.le, op.eq, op.ne, _in, _not_in
|
||||
_cmp_ops_dict = dict(zip(_cmp_ops_syms, _cmp_ops_funcs))
|
||||
|
||||
_bool_ops_syms = '&', '|', 'and', 'or'
|
||||
_bool_ops_funcs = op.and_, op.or_, op.and_, op.or_
|
||||
_bool_ops_dict = dict(zip(_bool_ops_syms, _bool_ops_funcs))
|
||||
|
||||
_arith_ops_syms = '+', '-', '*', '/', '**', '//', '%'
|
||||
_arith_ops_funcs = (op.add, op.sub, op.mul, op.truediv if PY3 else op.div,
|
||||
op.pow, op.floordiv, op.mod)
|
||||
_arith_ops_dict = dict(zip(_arith_ops_syms, _arith_ops_funcs))
|
||||
|
||||
_special_case_arith_ops_syms = '**', '//', '%'
|
||||
_special_case_arith_ops_funcs = op.pow, op.floordiv, op.mod
|
||||
_special_case_arith_ops_dict = dict(zip(_special_case_arith_ops_syms,
|
||||
_special_case_arith_ops_funcs))
|
||||
|
||||
_binary_ops_dict = {}
|
||||
|
||||
for d in (_cmp_ops_dict, _bool_ops_dict, _arith_ops_dict):
|
||||
_binary_ops_dict.update(d)
|
||||
|
||||
|
||||
def _cast_inplace(terms, acceptable_dtypes, dtype):
|
||||
"""Cast an expression inplace.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
terms : Op
|
||||
The expression that should cast.
|
||||
acceptable_dtypes : list of acceptable numpy.dtype
|
||||
Will not cast if term's dtype in this list.
|
||||
|
||||
.. versionadded:: 0.19.0
|
||||
|
||||
dtype : str or numpy.dtype
|
||||
The dtype to cast to.
|
||||
"""
|
||||
dt = np.dtype(dtype)
|
||||
for term in terms:
|
||||
if term.type in acceptable_dtypes:
|
||||
continue
|
||||
|
||||
try:
|
||||
new_value = term.value.astype(dt)
|
||||
except AttributeError:
|
||||
new_value = dt.type(term.value)
|
||||
term.update(new_value)
|
||||
|
||||
|
||||
def is_term(obj):
|
||||
return isinstance(obj, Term)
|
||||
|
||||
|
||||
class BinOp(Op):
|
||||
|
||||
"""Hold a binary operator and its operands
|
||||
|
||||
Parameters
|
||||
----------
|
||||
op : str
|
||||
left : Term or Op
|
||||
right : Term or Op
|
||||
"""
|
||||
|
||||
def __init__(self, op, lhs, rhs, **kwargs):
|
||||
super(BinOp, self).__init__(op, (lhs, rhs))
|
||||
self.lhs = lhs
|
||||
self.rhs = rhs
|
||||
|
||||
self._disallow_scalar_only_bool_ops()
|
||||
|
||||
self.convert_values()
|
||||
|
||||
try:
|
||||
self.func = _binary_ops_dict[op]
|
||||
except KeyError:
|
||||
# has to be made a list for python3
|
||||
keys = list(_binary_ops_dict.keys())
|
||||
raise ValueError('Invalid binary operator {0!r}, valid'
|
||||
' operators are {1}'.format(op, keys))
|
||||
|
||||
def __call__(self, env):
|
||||
"""Recursively evaluate an expression in Python space.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
env : Scope
|
||||
|
||||
Returns
|
||||
-------
|
||||
object
|
||||
The result of an evaluated expression.
|
||||
"""
|
||||
# handle truediv
|
||||
if self.op == '/' and env.scope['truediv']:
|
||||
self.func = op.truediv
|
||||
|
||||
# recurse over the left/right nodes
|
||||
left = self.lhs(env)
|
||||
right = self.rhs(env)
|
||||
|
||||
return self.func(left, right)
|
||||
|
||||
def evaluate(self, env, engine, parser, term_type, eval_in_python):
|
||||
"""Evaluate a binary operation *before* being passed to the engine.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
env : Scope
|
||||
engine : str
|
||||
parser : str
|
||||
term_type : type
|
||||
eval_in_python : list
|
||||
|
||||
Returns
|
||||
-------
|
||||
term_type
|
||||
The "pre-evaluated" expression as an instance of ``term_type``
|
||||
"""
|
||||
if engine == 'python':
|
||||
res = self(env)
|
||||
else:
|
||||
# recurse over the left/right nodes
|
||||
left = self.lhs.evaluate(env, engine=engine, parser=parser,
|
||||
term_type=term_type,
|
||||
eval_in_python=eval_in_python)
|
||||
right = self.rhs.evaluate(env, engine=engine, parser=parser,
|
||||
term_type=term_type,
|
||||
eval_in_python=eval_in_python)
|
||||
|
||||
# base cases
|
||||
if self.op in eval_in_python:
|
||||
res = self.func(left.value, right.value)
|
||||
else:
|
||||
res = pd.eval(self, local_dict=env, engine=engine,
|
||||
parser=parser)
|
||||
|
||||
name = env.add_tmp(res)
|
||||
return term_type(name, env=env)
|
||||
|
||||
def convert_values(self):
|
||||
"""Convert datetimes to a comparable value in an expression.
|
||||
"""
|
||||
def stringify(value):
|
||||
if self.encoding is not None:
|
||||
encoder = partial(pprint_thing_encoded,
|
||||
encoding=self.encoding)
|
||||
else:
|
||||
encoder = pprint_thing
|
||||
return encoder(value)
|
||||
|
||||
lhs, rhs = self.lhs, self.rhs
|
||||
|
||||
if is_term(lhs) and lhs.is_datetime and is_term(rhs) and rhs.is_scalar:
|
||||
v = rhs.value
|
||||
if isinstance(v, (int, float)):
|
||||
v = stringify(v)
|
||||
v = pd.Timestamp(_ensure_decoded(v))
|
||||
if v.tz is not None:
|
||||
v = v.tz_convert('UTC')
|
||||
self.rhs.update(v)
|
||||
|
||||
if is_term(rhs) and rhs.is_datetime and is_term(lhs) and lhs.is_scalar:
|
||||
v = lhs.value
|
||||
if isinstance(v, (int, float)):
|
||||
v = stringify(v)
|
||||
v = pd.Timestamp(_ensure_decoded(v))
|
||||
if v.tz is not None:
|
||||
v = v.tz_convert('UTC')
|
||||
self.lhs.update(v)
|
||||
|
||||
def _disallow_scalar_only_bool_ops(self):
|
||||
if ((self.lhs.is_scalar or self.rhs.is_scalar) and
|
||||
self.op in _bool_ops_dict and
|
||||
(not (issubclass(self.rhs.return_type, (bool, np.bool_)) and
|
||||
issubclass(self.lhs.return_type, (bool, np.bool_))))):
|
||||
raise NotImplementedError("cannot evaluate scalar only bool ops")
|
||||
|
||||
|
||||
def isnumeric(dtype):
|
||||
return issubclass(np.dtype(dtype).type, np.number)
|
||||
|
||||
|
||||
class Div(BinOp):
|
||||
|
||||
"""Div operator to special case casting.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
lhs, rhs : Term or Op
|
||||
The Terms or Ops in the ``/`` expression.
|
||||
truediv : bool
|
||||
Whether or not to use true division. With Python 3 this happens
|
||||
regardless of the value of ``truediv``.
|
||||
"""
|
||||
|
||||
def __init__(self, lhs, rhs, truediv, *args, **kwargs):
|
||||
super(Div, self).__init__('/', lhs, rhs, *args, **kwargs)
|
||||
|
||||
if not isnumeric(lhs.return_type) or not isnumeric(rhs.return_type):
|
||||
raise TypeError("unsupported operand type(s) for {0}:"
|
||||
" '{1}' and '{2}'".format(self.op,
|
||||
lhs.return_type,
|
||||
rhs.return_type))
|
||||
|
||||
if truediv or PY3:
|
||||
# do not upcast float32s to float64 un-necessarily
|
||||
acceptable_dtypes = [np.float32, np.float_]
|
||||
_cast_inplace(com.flatten(self), acceptable_dtypes, np.float_)
|
||||
|
||||
|
||||
_unary_ops_syms = '+', '-', '~', 'not'
|
||||
_unary_ops_funcs = op.pos, op.neg, op.invert, op.invert
|
||||
_unary_ops_dict = dict(zip(_unary_ops_syms, _unary_ops_funcs))
|
||||
|
||||
|
||||
class UnaryOp(Op):
|
||||
|
||||
"""Hold a unary operator and its operands
|
||||
|
||||
Parameters
|
||||
----------
|
||||
op : str
|
||||
The token used to represent the operator.
|
||||
operand : Term or Op
|
||||
The Term or Op operand to the operator.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
* If no function associated with the passed operator token is found.
|
||||
"""
|
||||
|
||||
def __init__(self, op, operand):
|
||||
super(UnaryOp, self).__init__(op, (operand,))
|
||||
self.operand = operand
|
||||
|
||||
try:
|
||||
self.func = _unary_ops_dict[op]
|
||||
except KeyError:
|
||||
raise ValueError('Invalid unary operator {0!r}, valid operators '
|
||||
'are {1}'.format(op, _unary_ops_syms))
|
||||
|
||||
def __call__(self, env):
|
||||
operand = self.operand(env)
|
||||
return self.func(operand)
|
||||
|
||||
def __unicode__(self):
|
||||
return pprint_thing('{0}({1})'.format(self.op, self.operand))
|
||||
|
||||
@property
|
||||
def return_type(self):
|
||||
operand = self.operand
|
||||
if operand.return_type == np.dtype('bool'):
|
||||
return np.dtype('bool')
|
||||
if (isinstance(operand, Op) and
|
||||
(operand.op in _cmp_ops_dict or operand.op in _bool_ops_dict)):
|
||||
return np.dtype('bool')
|
||||
return np.dtype('int')
|
||||
|
||||
|
||||
class MathCall(Op):
|
||||
|
||||
def __init__(self, func, args):
|
||||
super(MathCall, self).__init__(func.name, args)
|
||||
self.func = func
|
||||
|
||||
def __call__(self, env):
|
||||
operands = [op(env) for op in self.operands]
|
||||
with np.errstate(all='ignore'):
|
||||
return self.func.func(*operands)
|
||||
|
||||
def __unicode__(self):
|
||||
operands = map(str, self.operands)
|
||||
return pprint_thing('{0}({1})'.format(self.op, ','.join(operands)))
|
||||
|
||||
|
||||
class FuncNode(object):
|
||||
|
||||
def __init__(self, name):
|
||||
if name not in _mathops:
|
||||
raise ValueError(
|
||||
"\"{0}\" is not a supported function".format(name))
|
||||
self.name = name
|
||||
self.func = getattr(np, name)
|
||||
|
||||
def __call__(self, *args):
|
||||
return MathCall(self, args)
|
||||
@@ -0,0 +1,601 @@
|
||||
""" manage PyTables query interface via Expressions """
|
||||
|
||||
import ast
|
||||
from functools import partial
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from pandas.core.dtypes.common import is_list_like
|
||||
import pandas.core.common as com
|
||||
from pandas.compat import u, string_types, DeepChainMap
|
||||
from pandas.core.base import StringMixin
|
||||
from pandas.io.formats.printing import pprint_thing, pprint_thing_encoded
|
||||
from pandas.core.computation import expr, ops
|
||||
from pandas.core.computation.ops import is_term, UndefinedVariableError
|
||||
from pandas.core.computation.expr import BaseExprVisitor
|
||||
from pandas.core.computation.common import _ensure_decoded
|
||||
from pandas.core.tools.timedeltas import _coerce_scalar_to_timedelta_type
|
||||
|
||||
|
||||
class Scope(expr.Scope):
|
||||
__slots__ = 'queryables',
|
||||
|
||||
def __init__(self, level, global_dict=None, local_dict=None,
|
||||
queryables=None):
|
||||
super(Scope, self).__init__(level + 1, global_dict=global_dict,
|
||||
local_dict=local_dict)
|
||||
self.queryables = queryables or dict()
|
||||
|
||||
|
||||
class Term(ops.Term):
|
||||
|
||||
def __new__(cls, name, env, side=None, encoding=None):
|
||||
klass = Constant if not isinstance(name, string_types) else cls
|
||||
supr_new = StringMixin.__new__
|
||||
return supr_new(klass)
|
||||
|
||||
def __init__(self, name, env, side=None, encoding=None):
|
||||
super(Term, self).__init__(name, env, side=side, encoding=encoding)
|
||||
|
||||
def _resolve_name(self):
|
||||
# must be a queryables
|
||||
if self.side == 'left':
|
||||
if self.name not in self.env.queryables:
|
||||
raise NameError('name {name!r} is not defined'
|
||||
.format(name=self.name))
|
||||
return self.name
|
||||
|
||||
# resolve the rhs (and allow it to be None)
|
||||
try:
|
||||
return self.env.resolve(self.name, is_local=False)
|
||||
except UndefinedVariableError:
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self._value
|
||||
|
||||
|
||||
class Constant(Term):
|
||||
|
||||
def __init__(self, value, env, side=None, encoding=None):
|
||||
super(Constant, self).__init__(value, env, side=side,
|
||||
encoding=encoding)
|
||||
|
||||
def _resolve_name(self):
|
||||
return self._name
|
||||
|
||||
|
||||
class BinOp(ops.BinOp):
|
||||
|
||||
_max_selectors = 31
|
||||
|
||||
def __init__(self, op, lhs, rhs, queryables, encoding):
|
||||
super(BinOp, self).__init__(op, lhs, rhs)
|
||||
self.queryables = queryables
|
||||
self.encoding = encoding
|
||||
self.filter = None
|
||||
self.condition = None
|
||||
|
||||
def _disallow_scalar_only_bool_ops(self):
|
||||
pass
|
||||
|
||||
def prune(self, klass):
|
||||
|
||||
def pr(left, right):
|
||||
""" create and return a new specialized BinOp from myself """
|
||||
|
||||
if left is None:
|
||||
return right
|
||||
elif right is None:
|
||||
return left
|
||||
|
||||
k = klass
|
||||
if isinstance(left, ConditionBinOp):
|
||||
if (isinstance(left, ConditionBinOp) and
|
||||
isinstance(right, ConditionBinOp)):
|
||||
k = JointConditionBinOp
|
||||
elif isinstance(left, k):
|
||||
return left
|
||||
elif isinstance(right, k):
|
||||
return right
|
||||
|
||||
elif isinstance(left, FilterBinOp):
|
||||
if (isinstance(left, FilterBinOp) and
|
||||
isinstance(right, FilterBinOp)):
|
||||
k = JointFilterBinOp
|
||||
elif isinstance(left, k):
|
||||
return left
|
||||
elif isinstance(right, k):
|
||||
return right
|
||||
|
||||
return k(self.op, left, right, queryables=self.queryables,
|
||||
encoding=self.encoding).evaluate()
|
||||
|
||||
left, right = self.lhs, self.rhs
|
||||
|
||||
if is_term(left) and is_term(right):
|
||||
res = pr(left.value, right.value)
|
||||
elif not is_term(left) and is_term(right):
|
||||
res = pr(left.prune(klass), right.value)
|
||||
elif is_term(left) and not is_term(right):
|
||||
res = pr(left.value, right.prune(klass))
|
||||
elif not (is_term(left) or is_term(right)):
|
||||
res = pr(left.prune(klass), right.prune(klass))
|
||||
|
||||
return res
|
||||
|
||||
def conform(self, rhs):
|
||||
""" inplace conform rhs """
|
||||
if not is_list_like(rhs):
|
||||
rhs = [rhs]
|
||||
if isinstance(rhs, np.ndarray):
|
||||
rhs = rhs.ravel()
|
||||
return rhs
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
""" return True if this is a valid field """
|
||||
return self.lhs in self.queryables
|
||||
|
||||
@property
|
||||
def is_in_table(self):
|
||||
""" return True if this is a valid column name for generation (e.g. an
|
||||
actual column in the table) """
|
||||
return self.queryables.get(self.lhs) is not None
|
||||
|
||||
@property
|
||||
def kind(self):
|
||||
""" the kind of my field """
|
||||
return getattr(self.queryables.get(self.lhs), 'kind', None)
|
||||
|
||||
@property
|
||||
def meta(self):
|
||||
""" the meta of my field """
|
||||
return getattr(self.queryables.get(self.lhs), 'meta', None)
|
||||
|
||||
@property
|
||||
def metadata(self):
|
||||
""" the metadata of my field """
|
||||
return getattr(self.queryables.get(self.lhs), 'metadata', None)
|
||||
|
||||
def generate(self, v):
|
||||
""" create and return the op string for this TermValue """
|
||||
val = v.tostring(self.encoding)
|
||||
return "({lhs} {op} {val})".format(lhs=self.lhs, op=self.op, val=val)
|
||||
|
||||
def convert_value(self, v):
|
||||
""" convert the expression that is in the term to something that is
|
||||
accepted by pytables """
|
||||
|
||||
def stringify(value):
|
||||
if self.encoding is not None:
|
||||
encoder = partial(pprint_thing_encoded,
|
||||
encoding=self.encoding)
|
||||
else:
|
||||
encoder = pprint_thing
|
||||
return encoder(value)
|
||||
|
||||
kind = _ensure_decoded(self.kind)
|
||||
meta = _ensure_decoded(self.meta)
|
||||
if kind == u('datetime64') or kind == u('datetime'):
|
||||
if isinstance(v, (int, float)):
|
||||
v = stringify(v)
|
||||
v = _ensure_decoded(v)
|
||||
v = pd.Timestamp(v)
|
||||
if v.tz is not None:
|
||||
v = v.tz_convert('UTC')
|
||||
return TermValue(v, v.value, kind)
|
||||
elif kind == u('timedelta64') or kind == u('timedelta'):
|
||||
v = _coerce_scalar_to_timedelta_type(v, unit='s').value
|
||||
return TermValue(int(v), v, kind)
|
||||
elif meta == u('category'):
|
||||
metadata = com._values_from_object(self.metadata)
|
||||
result = metadata.searchsorted(v, side='left')
|
||||
|
||||
# result returns 0 if v is first element or if v is not in metadata
|
||||
# check that metadata contains v
|
||||
if not result and v not in metadata:
|
||||
result = -1
|
||||
return TermValue(result, result, u('integer'))
|
||||
elif kind == u('integer'):
|
||||
v = int(float(v))
|
||||
return TermValue(v, v, kind)
|
||||
elif kind == u('float'):
|
||||
v = float(v)
|
||||
return TermValue(v, v, kind)
|
||||
elif kind == u('bool'):
|
||||
if isinstance(v, string_types):
|
||||
v = not v.strip().lower() in [u('false'), u('f'), u('no'),
|
||||
u('n'), u('none'), u('0'),
|
||||
u('[]'), u('{}'), u('')]
|
||||
else:
|
||||
v = bool(v)
|
||||
return TermValue(v, v, kind)
|
||||
elif isinstance(v, string_types):
|
||||
# string quoting
|
||||
return TermValue(v, stringify(v), u('string'))
|
||||
else:
|
||||
raise TypeError("Cannot compare {v} of type {typ} to {kind} column"
|
||||
.format(v=v, typ=type(v), kind=kind))
|
||||
|
||||
def convert_values(self):
|
||||
pass
|
||||
|
||||
|
||||
class FilterBinOp(BinOp):
|
||||
|
||||
def __unicode__(self):
|
||||
return pprint_thing("[Filter : [{lhs}] -> [{op}]"
|
||||
.format(lhs=self.filter[0], op=self.filter[1]))
|
||||
|
||||
def invert(self):
|
||||
""" invert the filter """
|
||||
if self.filter is not None:
|
||||
f = list(self.filter)
|
||||
f[1] = self.generate_filter_op(invert=True)
|
||||
self.filter = tuple(f)
|
||||
return self
|
||||
|
||||
def format(self):
|
||||
""" return the actual filter format """
|
||||
return [self.filter]
|
||||
|
||||
def evaluate(self):
|
||||
|
||||
if not self.is_valid:
|
||||
raise ValueError("query term is not valid [{slf}]"
|
||||
.format(slf=self))
|
||||
|
||||
rhs = self.conform(self.rhs)
|
||||
values = [TermValue(v, v, self.kind) for v in rhs]
|
||||
|
||||
if self.is_in_table:
|
||||
|
||||
# if too many values to create the expression, use a filter instead
|
||||
if self.op in ['==', '!='] and len(values) > self._max_selectors:
|
||||
|
||||
filter_op = self.generate_filter_op()
|
||||
self.filter = (
|
||||
self.lhs,
|
||||
filter_op,
|
||||
pd.Index([v.value for v in values]))
|
||||
|
||||
return self
|
||||
return None
|
||||
|
||||
# equality conditions
|
||||
if self.op in ['==', '!=']:
|
||||
|
||||
filter_op = self.generate_filter_op()
|
||||
self.filter = (
|
||||
self.lhs,
|
||||
filter_op,
|
||||
pd.Index([v.value for v in values]))
|
||||
|
||||
else:
|
||||
raise TypeError("passing a filterable condition to a non-table "
|
||||
"indexer [{slf}]".format(slf=self))
|
||||
|
||||
return self
|
||||
|
||||
def generate_filter_op(self, invert=False):
|
||||
if (self.op == '!=' and not invert) or (self.op == '==' and invert):
|
||||
return lambda axis, vals: ~axis.isin(vals)
|
||||
else:
|
||||
return lambda axis, vals: axis.isin(vals)
|
||||
|
||||
|
||||
class JointFilterBinOp(FilterBinOp):
|
||||
|
||||
def format(self):
|
||||
raise NotImplementedError("unable to collapse Joint Filters")
|
||||
|
||||
def evaluate(self):
|
||||
return self
|
||||
|
||||
|
||||
class ConditionBinOp(BinOp):
|
||||
|
||||
def __unicode__(self):
|
||||
return pprint_thing("[Condition : [{cond}]]"
|
||||
.format(cond=self.condition))
|
||||
|
||||
def invert(self):
|
||||
""" invert the condition """
|
||||
# if self.condition is not None:
|
||||
# self.condition = "~(%s)" % self.condition
|
||||
# return self
|
||||
raise NotImplementedError("cannot use an invert condition when "
|
||||
"passing to numexpr")
|
||||
|
||||
def format(self):
|
||||
""" return the actual ne format """
|
||||
return self.condition
|
||||
|
||||
def evaluate(self):
|
||||
|
||||
if not self.is_valid:
|
||||
raise ValueError("query term is not valid [{slf}]"
|
||||
.format(slf=self))
|
||||
|
||||
# convert values if we are in the table
|
||||
if not self.is_in_table:
|
||||
return None
|
||||
|
||||
rhs = self.conform(self.rhs)
|
||||
values = [self.convert_value(v) for v in rhs]
|
||||
|
||||
# equality conditions
|
||||
if self.op in ['==', '!=']:
|
||||
|
||||
# too many values to create the expression?
|
||||
if len(values) <= self._max_selectors:
|
||||
vs = [self.generate(v) for v in values]
|
||||
self.condition = "({cond})".format(cond=' | '.join(vs))
|
||||
|
||||
# use a filter after reading
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
self.condition = self.generate(values[0])
|
||||
|
||||
return self
|
||||
|
||||
|
||||
class JointConditionBinOp(ConditionBinOp):
|
||||
|
||||
def evaluate(self):
|
||||
self.condition = "({lhs} {op} {rhs})".format(lhs=self.lhs.condition,
|
||||
op=self.op,
|
||||
rhs=self.rhs.condition)
|
||||
return self
|
||||
|
||||
|
||||
class UnaryOp(ops.UnaryOp):
|
||||
|
||||
def prune(self, klass):
|
||||
|
||||
if self.op != '~':
|
||||
raise NotImplementedError("UnaryOp only support invert type ops")
|
||||
|
||||
operand = self.operand
|
||||
operand = operand.prune(klass)
|
||||
|
||||
if operand is not None:
|
||||
if issubclass(klass, ConditionBinOp):
|
||||
if operand.condition is not None:
|
||||
return operand.invert()
|
||||
elif issubclass(klass, FilterBinOp):
|
||||
if operand.filter is not None:
|
||||
return operand.invert()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
_op_classes = {'unary': UnaryOp}
|
||||
|
||||
|
||||
class ExprVisitor(BaseExprVisitor):
|
||||
const_type = Constant
|
||||
term_type = Term
|
||||
|
||||
def __init__(self, env, engine, parser, **kwargs):
|
||||
super(ExprVisitor, self).__init__(env, engine, parser)
|
||||
for bin_op in self.binary_ops:
|
||||
bin_node = self.binary_op_nodes_map[bin_op]
|
||||
setattr(self, 'visit_{node}'.format(node=bin_node),
|
||||
lambda node, bin_op=bin_op: partial(BinOp, bin_op,
|
||||
**kwargs))
|
||||
|
||||
def visit_UnaryOp(self, node, **kwargs):
|
||||
if isinstance(node.op, (ast.Not, ast.Invert)):
|
||||
return UnaryOp('~', self.visit(node.operand))
|
||||
elif isinstance(node.op, ast.USub):
|
||||
return self.const_type(-self.visit(node.operand).value, self.env)
|
||||
elif isinstance(node.op, ast.UAdd):
|
||||
raise NotImplementedError('Unary addition not supported')
|
||||
|
||||
def visit_Index(self, node, **kwargs):
|
||||
return self.visit(node.value).value
|
||||
|
||||
def visit_Assign(self, node, **kwargs):
|
||||
cmpr = ast.Compare(ops=[ast.Eq()], left=node.targets[0],
|
||||
comparators=[node.value])
|
||||
return self.visit(cmpr)
|
||||
|
||||
def visit_Subscript(self, node, **kwargs):
|
||||
# only allow simple suscripts
|
||||
|
||||
value = self.visit(node.value)
|
||||
slobj = self.visit(node.slice)
|
||||
try:
|
||||
value = value.value
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
return self.const_type(value[slobj], self.env)
|
||||
except TypeError:
|
||||
raise ValueError("cannot subscript {value!r} with "
|
||||
"{slobj!r}".format(value=value, slobj=slobj))
|
||||
|
||||
def visit_Attribute(self, node, **kwargs):
|
||||
attr = node.attr
|
||||
value = node.value
|
||||
|
||||
ctx = node.ctx.__class__
|
||||
if ctx == ast.Load:
|
||||
# resolve the value
|
||||
resolved = self.visit(value)
|
||||
|
||||
# try to get the value to see if we are another expression
|
||||
try:
|
||||
resolved = resolved.value
|
||||
except (AttributeError):
|
||||
pass
|
||||
|
||||
try:
|
||||
return self.term_type(getattr(resolved, attr), self.env)
|
||||
except AttributeError:
|
||||
|
||||
# something like datetime.datetime where scope is overridden
|
||||
if isinstance(value, ast.Name) and value.id == attr:
|
||||
return resolved
|
||||
|
||||
raise ValueError("Invalid Attribute context {name}"
|
||||
.format(name=ctx.__name__))
|
||||
|
||||
def translate_In(self, op):
|
||||
return ast.Eq() if isinstance(op, ast.In) else op
|
||||
|
||||
def _rewrite_membership_op(self, node, left, right):
|
||||
return self.visit(node.op), node.op, left, right
|
||||
|
||||
|
||||
def _validate_where(w):
|
||||
"""
|
||||
Validate that the where statement is of the right type.
|
||||
|
||||
The type may either be String, Expr, or list-like of Exprs.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
w : String term expression, Expr, or list-like of Exprs.
|
||||
|
||||
Returns
|
||||
-------
|
||||
where : The original where clause if the check was successful.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError : An invalid data type was passed in for w (e.g. dict).
|
||||
"""
|
||||
|
||||
if not (isinstance(w, (Expr, string_types)) or is_list_like(w)):
|
||||
raise TypeError("where must be passed as a string, Expr, "
|
||||
"or list-like of Exprs")
|
||||
|
||||
return w
|
||||
|
||||
|
||||
class Expr(expr.Expr):
|
||||
|
||||
""" hold a pytables like expression, comprised of possibly multiple 'terms'
|
||||
|
||||
Parameters
|
||||
----------
|
||||
where : string term expression, Expr, or list-like of Exprs
|
||||
queryables : a "kinds" map (dict of column name -> kind), or None if column
|
||||
is non-indexable
|
||||
encoding : an encoding that will encode the query terms
|
||||
|
||||
Returns
|
||||
-------
|
||||
an Expr object
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
'index>=date'
|
||||
"columns=['A', 'D']"
|
||||
'columns=A'
|
||||
'columns==A'
|
||||
"~(columns=['A','B'])"
|
||||
'index>df.index[3] & string="bar"'
|
||||
'(index>df.index[3] & index<=df.index[6]) | string="bar"'
|
||||
"ts>=Timestamp('2012-02-01')"
|
||||
"major_axis>=20130101"
|
||||
"""
|
||||
|
||||
def __init__(self, where, queryables=None, encoding=None, scope_level=0):
|
||||
|
||||
where = _validate_where(where)
|
||||
|
||||
self.encoding = encoding
|
||||
self.condition = None
|
||||
self.filter = None
|
||||
self.terms = None
|
||||
self._visitor = None
|
||||
|
||||
# capture the environment if needed
|
||||
local_dict = DeepChainMap()
|
||||
|
||||
if isinstance(where, Expr):
|
||||
local_dict = where.env.scope
|
||||
where = where.expr
|
||||
|
||||
elif isinstance(where, (list, tuple)):
|
||||
for idx, w in enumerate(where):
|
||||
if isinstance(w, Expr):
|
||||
local_dict = w.env.scope
|
||||
else:
|
||||
w = _validate_where(w)
|
||||
where[idx] = w
|
||||
where = ' & '.join(map('({})'.format, com.flatten(where))) # noqa
|
||||
|
||||
self.expr = where
|
||||
self.env = Scope(scope_level + 1, local_dict=local_dict)
|
||||
|
||||
if queryables is not None and isinstance(self.expr, string_types):
|
||||
self.env.queryables.update(queryables)
|
||||
self._visitor = ExprVisitor(self.env, queryables=queryables,
|
||||
parser='pytables', engine='pytables',
|
||||
encoding=encoding)
|
||||
self.terms = self.parse()
|
||||
|
||||
def __unicode__(self):
|
||||
if self.terms is not None:
|
||||
return pprint_thing(self.terms)
|
||||
return pprint_thing(self.expr)
|
||||
|
||||
def evaluate(self):
|
||||
""" create and return the numexpr condition and filter """
|
||||
|
||||
try:
|
||||
self.condition = self.terms.prune(ConditionBinOp)
|
||||
except AttributeError:
|
||||
raise ValueError("cannot process expression [{expr}], [{slf}] "
|
||||
"is not a valid condition".format(expr=self.expr,
|
||||
slf=self))
|
||||
try:
|
||||
self.filter = self.terms.prune(FilterBinOp)
|
||||
except AttributeError:
|
||||
raise ValueError("cannot process expression [{expr}], [{slf}] "
|
||||
"is not a valid filter".format(expr=self.expr,
|
||||
slf=self))
|
||||
|
||||
return self.condition, self.filter
|
||||
|
||||
|
||||
class TermValue(object):
|
||||
|
||||
""" hold a term value the we use to construct a condition/filter """
|
||||
|
||||
def __init__(self, value, converted, kind):
|
||||
self.value = value
|
||||
self.converted = converted
|
||||
self.kind = kind
|
||||
|
||||
def tostring(self, encoding):
|
||||
""" quote the string if not encoded
|
||||
else encode and return """
|
||||
if self.kind == u'string':
|
||||
if encoding is not None:
|
||||
return self.converted
|
||||
return '"{converted}"'.format(converted=self.converted)
|
||||
elif self.kind == u'float':
|
||||
# python 2 str(float) is not always
|
||||
# round-trippable so use repr()
|
||||
return repr(self.converted)
|
||||
return self.converted
|
||||
|
||||
|
||||
def maybe_expression(s):
|
||||
""" loose checking if s is a pytables-acceptable expression """
|
||||
if not isinstance(s, string_types):
|
||||
return False
|
||||
ops = ExprVisitor.binary_ops + ExprVisitor.unary_ops + ('=',)
|
||||
|
||||
# make sure we have an op at least
|
||||
return any(op in s for op in ops)
|
||||
@@ -0,0 +1,302 @@
|
||||
"""
|
||||
Module for scope operations
|
||||
"""
|
||||
|
||||
import sys
|
||||
import struct
|
||||
import inspect
|
||||
import datetime
|
||||
import itertools
|
||||
import pprint
|
||||
|
||||
import numpy as np
|
||||
|
||||
import pandas
|
||||
import pandas as pd # noqa
|
||||
from pandas.compat import DeepChainMap, map, StringIO
|
||||
from pandas.core.base import StringMixin
|
||||
import pandas.core.computation as compu
|
||||
|
||||
|
||||
def _ensure_scope(level, global_dict=None, local_dict=None, resolvers=(),
|
||||
target=None, **kwargs):
|
||||
"""Ensure that we are grabbing the correct scope."""
|
||||
return Scope(level + 1, global_dict=global_dict, local_dict=local_dict,
|
||||
resolvers=resolvers, target=target)
|
||||
|
||||
|
||||
def _replacer(x):
|
||||
"""Replace a number with its hexadecimal representation. Used to tag
|
||||
temporary variables with their calling scope's id.
|
||||
"""
|
||||
# get the hex repr of the binary char and remove 0x and pad by pad_size
|
||||
# zeros
|
||||
try:
|
||||
hexin = ord(x)
|
||||
except TypeError:
|
||||
# bytes literals masquerade as ints when iterating in py3
|
||||
hexin = x
|
||||
|
||||
return hex(hexin)
|
||||
|
||||
|
||||
def _raw_hex_id(obj):
|
||||
"""Return the padded hexadecimal id of ``obj``."""
|
||||
# interpret as a pointer since that's what really what id returns
|
||||
packed = struct.pack('@P', id(obj))
|
||||
return ''.join(map(_replacer, packed))
|
||||
|
||||
|
||||
_DEFAULT_GLOBALS = {
|
||||
'Timestamp': pandas._libs.tslib.Timestamp,
|
||||
'datetime': datetime.datetime,
|
||||
'True': True,
|
||||
'False': False,
|
||||
'list': list,
|
||||
'tuple': tuple,
|
||||
'inf': np.inf,
|
||||
'Inf': np.inf,
|
||||
}
|
||||
|
||||
|
||||
def _get_pretty_string(obj):
|
||||
"""Return a prettier version of obj
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : object
|
||||
Object to pretty print
|
||||
|
||||
Returns
|
||||
-------
|
||||
s : str
|
||||
Pretty print object repr
|
||||
"""
|
||||
sio = StringIO()
|
||||
pprint.pprint(obj, stream=sio)
|
||||
return sio.getvalue()
|
||||
|
||||
|
||||
class Scope(StringMixin):
|
||||
|
||||
"""Object to hold scope, with a few bells to deal with some custom syntax
|
||||
and contexts added by pandas.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
level : int
|
||||
global_dict : dict or None, optional, default None
|
||||
local_dict : dict or Scope or None, optional, default None
|
||||
resolvers : list-like or None, optional, default None
|
||||
target : object
|
||||
|
||||
Attributes
|
||||
----------
|
||||
level : int
|
||||
scope : DeepChainMap
|
||||
target : object
|
||||
temps : dict
|
||||
"""
|
||||
__slots__ = 'level', 'scope', 'target', 'temps'
|
||||
|
||||
def __init__(self, level, global_dict=None, local_dict=None, resolvers=(),
|
||||
target=None):
|
||||
self.level = level + 1
|
||||
|
||||
# shallow copy because we don't want to keep filling this up with what
|
||||
# was there before if there are multiple calls to Scope/_ensure_scope
|
||||
self.scope = DeepChainMap(_DEFAULT_GLOBALS.copy())
|
||||
self.target = target
|
||||
|
||||
if isinstance(local_dict, Scope):
|
||||
self.scope.update(local_dict.scope)
|
||||
if local_dict.target is not None:
|
||||
self.target = local_dict.target
|
||||
self.update(local_dict.level)
|
||||
|
||||
frame = sys._getframe(self.level)
|
||||
|
||||
try:
|
||||
# shallow copy here because we don't want to replace what's in
|
||||
# scope when we align terms (alignment accesses the underlying
|
||||
# numpy array of pandas objects)
|
||||
self.scope = self.scope.new_child((global_dict or
|
||||
frame.f_globals).copy())
|
||||
if not isinstance(local_dict, Scope):
|
||||
self.scope = self.scope.new_child((local_dict or
|
||||
frame.f_locals).copy())
|
||||
finally:
|
||||
del frame
|
||||
|
||||
# assumes that resolvers are going from outermost scope to inner
|
||||
if isinstance(local_dict, Scope):
|
||||
resolvers += tuple(local_dict.resolvers.maps)
|
||||
self.resolvers = DeepChainMap(*resolvers)
|
||||
self.temps = {}
|
||||
|
||||
def __unicode__(self):
|
||||
scope_keys = _get_pretty_string(list(self.scope.keys()))
|
||||
res_keys = _get_pretty_string(list(self.resolvers.keys()))
|
||||
unicode_str = '{name}(scope={scope_keys}, resolvers={res_keys})'
|
||||
return unicode_str.format(name=type(self).__name__,
|
||||
scope_keys=scope_keys,
|
||||
res_keys=res_keys)
|
||||
|
||||
@property
|
||||
def has_resolvers(self):
|
||||
"""Return whether we have any extra scope.
|
||||
|
||||
For example, DataFrames pass Their columns as resolvers during calls to
|
||||
``DataFrame.eval()`` and ``DataFrame.query()``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
hr : bool
|
||||
"""
|
||||
return bool(len(self.resolvers))
|
||||
|
||||
def resolve(self, key, is_local):
|
||||
"""Resolve a variable name in a possibly local context
|
||||
|
||||
Parameters
|
||||
----------
|
||||
key : text_type
|
||||
A variable name
|
||||
is_local : bool
|
||||
Flag indicating whether the variable is local or not (prefixed with
|
||||
the '@' symbol)
|
||||
|
||||
Returns
|
||||
-------
|
||||
value : object
|
||||
The value of a particular variable
|
||||
"""
|
||||
try:
|
||||
# only look for locals in outer scope
|
||||
if is_local:
|
||||
return self.scope[key]
|
||||
|
||||
# not a local variable so check in resolvers if we have them
|
||||
if self.has_resolvers:
|
||||
return self.resolvers[key]
|
||||
|
||||
# if we're here that means that we have no locals and we also have
|
||||
# no resolvers
|
||||
assert not is_local and not self.has_resolvers
|
||||
return self.scope[key]
|
||||
except KeyError:
|
||||
try:
|
||||
# last ditch effort we look in temporaries
|
||||
# these are created when parsing indexing expressions
|
||||
# e.g., df[df > 0]
|
||||
return self.temps[key]
|
||||
except KeyError:
|
||||
raise compu.ops.UndefinedVariableError(key, is_local)
|
||||
|
||||
def swapkey(self, old_key, new_key, new_value=None):
|
||||
"""Replace a variable name, with a potentially new value.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
old_key : str
|
||||
Current variable name to replace
|
||||
new_key : str
|
||||
New variable name to replace `old_key` with
|
||||
new_value : object
|
||||
Value to be replaced along with the possible renaming
|
||||
"""
|
||||
if self.has_resolvers:
|
||||
maps = self.resolvers.maps + self.scope.maps
|
||||
else:
|
||||
maps = self.scope.maps
|
||||
|
||||
maps.append(self.temps)
|
||||
|
||||
for mapping in maps:
|
||||
if old_key in mapping:
|
||||
mapping[new_key] = new_value
|
||||
return
|
||||
|
||||
def _get_vars(self, stack, scopes):
|
||||
"""Get specifically scoped variables from a list of stack frames.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
stack : list
|
||||
A list of stack frames as returned by ``inspect.stack()``
|
||||
scopes : sequence of strings
|
||||
A sequence containing valid stack frame attribute names that
|
||||
evaluate to a dictionary. For example, ('locals', 'globals')
|
||||
"""
|
||||
variables = itertools.product(scopes, stack)
|
||||
for scope, (frame, _, _, _, _, _) in variables:
|
||||
try:
|
||||
d = getattr(frame, 'f_' + scope)
|
||||
self.scope = self.scope.new_child(d)
|
||||
finally:
|
||||
# won't remove it, but DECREF it
|
||||
# in Py3 this probably isn't necessary since frame won't be
|
||||
# scope after the loop
|
||||
del frame
|
||||
|
||||
def update(self, level):
|
||||
"""Update the current scope by going back `level` levels.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
level : int or None, optional, default None
|
||||
"""
|
||||
sl = level + 1
|
||||
|
||||
# add sl frames to the scope starting with the
|
||||
# most distant and overwriting with more current
|
||||
# makes sure that we can capture variable scope
|
||||
stack = inspect.stack()
|
||||
|
||||
try:
|
||||
self._get_vars(stack[:sl], scopes=['locals'])
|
||||
finally:
|
||||
del stack[:], stack
|
||||
|
||||
def add_tmp(self, value):
|
||||
"""Add a temporary variable to the scope.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : object
|
||||
An arbitrary object to be assigned to a temporary variable.
|
||||
|
||||
Returns
|
||||
-------
|
||||
name : basestring
|
||||
The name of the temporary variable created.
|
||||
"""
|
||||
name = '{name}_{num}_{hex_id}'.format(name=type(value).__name__,
|
||||
num=self.ntemps,
|
||||
hex_id=_raw_hex_id(self))
|
||||
|
||||
# add to inner most scope
|
||||
assert name not in self.temps
|
||||
self.temps[name] = value
|
||||
assert name in self.temps
|
||||
|
||||
# only increment if the variable gets put in the scope
|
||||
return name
|
||||
|
||||
@property
|
||||
def ntemps(self):
|
||||
"""The number of temporary variables in this scope"""
|
||||
return len(self.temps)
|
||||
|
||||
@property
|
||||
def full_scope(self):
|
||||
"""Return the full scope for use with passing to engines transparently
|
||||
as a mapping.
|
||||
|
||||
Returns
|
||||
-------
|
||||
vars : DeepChainMap
|
||||
All variables in this scope.
|
||||
"""
|
||||
maps = [self.temps] + self.resolvers.maps + self.scope.maps
|
||||
return DeepChainMap(*maps)
|
||||
@@ -0,0 +1,841 @@
|
||||
"""
|
||||
The config module holds package-wide configurables and provides
|
||||
a uniform API for working with them.
|
||||
|
||||
Overview
|
||||
========
|
||||
|
||||
This module supports the following requirements:
|
||||
- options are referenced using keys in dot.notation, e.g. "x.y.option - z".
|
||||
- keys are case-insensitive.
|
||||
- functions should accept partial/regex keys, when unambiguous.
|
||||
- options can be registered by modules at import time.
|
||||
- options can be registered at init-time (via core.config_init)
|
||||
- options have a default value, and (optionally) a description and
|
||||
validation function associated with them.
|
||||
- options can be deprecated, in which case referencing them
|
||||
should produce a warning.
|
||||
- deprecated options can optionally be rerouted to a replacement
|
||||
so that accessing a deprecated option reroutes to a differently
|
||||
named option.
|
||||
- options can be reset to their default value.
|
||||
- all option can be reset to their default value at once.
|
||||
- all options in a certain sub - namespace can be reset at once.
|
||||
- the user can set / get / reset or ask for the description of an option.
|
||||
- a developer can register and mark an option as deprecated.
|
||||
- you can register a callback to be invoked when the option value
|
||||
is set or reset. Changing the stored value is considered misuse, but
|
||||
is not verboten.
|
||||
|
||||
Implementation
|
||||
==============
|
||||
|
||||
- Data is stored using nested dictionaries, and should be accessed
|
||||
through the provided API.
|
||||
|
||||
- "Registered options" and "Deprecated options" have metadata associated
|
||||
with them, which are stored in auxiliary dictionaries keyed on the
|
||||
fully-qualified key, e.g. "x.y.z.option".
|
||||
|
||||
- the config_init module is imported by the package's __init__.py file.
|
||||
placing any register_option() calls there will ensure those options
|
||||
are available as soon as pandas is loaded. If you use register_option
|
||||
in a module, it will only be available after that module is imported,
|
||||
which you should be aware of.
|
||||
|
||||
- `config_prefix` is a context_manager (for use with the `with` keyword)
|
||||
which can save developers some typing, see the docstring.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from collections import namedtuple
|
||||
from contextlib import contextmanager
|
||||
import warnings
|
||||
from pandas.compat import map, lmap, u
|
||||
import pandas.compat as compat
|
||||
|
||||
DeprecatedOption = namedtuple('DeprecatedOption', 'key msg rkey removal_ver')
|
||||
RegisteredOption = namedtuple('RegisteredOption',
|
||||
'key defval doc validator cb')
|
||||
|
||||
_deprecated_options = {} # holds deprecated option metdata
|
||||
_registered_options = {} # holds registered option metdata
|
||||
_global_config = {} # holds the current values for registered options
|
||||
_reserved_keys = ['all'] # keys which have a special meaning
|
||||
|
||||
|
||||
class OptionError(AttributeError, KeyError):
|
||||
"""Exception for pandas.options, backwards compatible with KeyError
|
||||
checks
|
||||
"""
|
||||
|
||||
#
|
||||
# User API
|
||||
|
||||
|
||||
def _get_single_key(pat, silent):
|
||||
keys = _select_options(pat)
|
||||
if len(keys) == 0:
|
||||
if not silent:
|
||||
_warn_if_deprecated(pat)
|
||||
raise OptionError('No such keys(s): {pat!r}'.format(pat=pat))
|
||||
if len(keys) > 1:
|
||||
raise OptionError('Pattern matched multiple keys')
|
||||
key = keys[0]
|
||||
|
||||
if not silent:
|
||||
_warn_if_deprecated(key)
|
||||
|
||||
key = _translate_key(key)
|
||||
|
||||
return key
|
||||
|
||||
|
||||
def _get_option(pat, silent=False):
|
||||
key = _get_single_key(pat, silent)
|
||||
|
||||
# walk the nested dict
|
||||
root, k = _get_root(key)
|
||||
return root[k]
|
||||
|
||||
|
||||
def _set_option(*args, **kwargs):
|
||||
# must at least 1 arg deal with constraints later
|
||||
nargs = len(args)
|
||||
if not nargs or nargs % 2 != 0:
|
||||
raise ValueError("Must provide an even number of non-keyword "
|
||||
"arguments")
|
||||
|
||||
# default to false
|
||||
silent = kwargs.pop('silent', False)
|
||||
|
||||
if kwargs:
|
||||
msg = '_set_option() got an unexpected keyword argument "{kwarg}"'
|
||||
raise TypeError(msg.format(list(kwargs.keys())[0]))
|
||||
|
||||
for k, v in zip(args[::2], args[1::2]):
|
||||
key = _get_single_key(k, silent)
|
||||
|
||||
o = _get_registered_option(key)
|
||||
if o and o.validator:
|
||||
o.validator(v)
|
||||
|
||||
# walk the nested dict
|
||||
root, k = _get_root(key)
|
||||
root[k] = v
|
||||
|
||||
if o.cb:
|
||||
if silent:
|
||||
with warnings.catch_warnings(record=True):
|
||||
o.cb(key)
|
||||
else:
|
||||
o.cb(key)
|
||||
|
||||
|
||||
def _describe_option(pat='', _print_desc=True):
|
||||
|
||||
keys = _select_options(pat)
|
||||
if len(keys) == 0:
|
||||
raise OptionError('No such keys(s)')
|
||||
|
||||
s = u('')
|
||||
for k in keys: # filter by pat
|
||||
s += _build_option_description(k)
|
||||
|
||||
if _print_desc:
|
||||
print(s)
|
||||
else:
|
||||
return s
|
||||
|
||||
|
||||
def _reset_option(pat, silent=False):
|
||||
|
||||
keys = _select_options(pat)
|
||||
|
||||
if len(keys) == 0:
|
||||
raise OptionError('No such keys(s)')
|
||||
|
||||
if len(keys) > 1 and len(pat) < 4 and pat != 'all':
|
||||
raise ValueError('You must specify at least 4 characters when '
|
||||
'resetting multiple keys, use the special keyword '
|
||||
'"all" to reset all the options to their default '
|
||||
'value')
|
||||
|
||||
for k in keys:
|
||||
_set_option(k, _registered_options[k].defval, silent=silent)
|
||||
|
||||
|
||||
def get_default_val(pat):
|
||||
key = _get_single_key(pat, silent=True)
|
||||
return _get_registered_option(key).defval
|
||||
|
||||
|
||||
class DictWrapper(object):
|
||||
""" provide attribute-style access to a nested dict"""
|
||||
|
||||
def __init__(self, d, prefix=""):
|
||||
object.__setattr__(self, "d", d)
|
||||
object.__setattr__(self, "prefix", prefix)
|
||||
|
||||
def __setattr__(self, key, val):
|
||||
prefix = object.__getattribute__(self, "prefix")
|
||||
if prefix:
|
||||
prefix += "."
|
||||
prefix += key
|
||||
# you can't set new keys
|
||||
# can you can't overwrite subtrees
|
||||
if key in self.d and not isinstance(self.d[key], dict):
|
||||
_set_option(prefix, val)
|
||||
else:
|
||||
raise OptionError("You can only set the value of existing options")
|
||||
|
||||
def __getattr__(self, key):
|
||||
prefix = object.__getattribute__(self, "prefix")
|
||||
if prefix:
|
||||
prefix += "."
|
||||
prefix += key
|
||||
try:
|
||||
v = object.__getattribute__(self, "d")[key]
|
||||
except KeyError:
|
||||
raise OptionError("No such option")
|
||||
if isinstance(v, dict):
|
||||
return DictWrapper(v, prefix)
|
||||
else:
|
||||
return _get_option(prefix)
|
||||
|
||||
def __dir__(self):
|
||||
return list(self.d.keys())
|
||||
|
||||
# For user convenience, we'd like to have the available options described
|
||||
# in the docstring. For dev convenience we'd like to generate the docstrings
|
||||
# dynamically instead of maintaining them by hand. To this, we use the
|
||||
# class below which wraps functions inside a callable, and converts
|
||||
# __doc__ into a property function. The doctsrings below are templates
|
||||
# using the py2.6+ advanced formatting syntax to plug in a concise list
|
||||
# of options, and option descriptions.
|
||||
|
||||
|
||||
class CallableDynamicDoc(object):
|
||||
|
||||
def __init__(self, func, doc_tmpl):
|
||||
self.__doc_tmpl__ = doc_tmpl
|
||||
self.__func__ = func
|
||||
|
||||
def __call__(self, *args, **kwds):
|
||||
return self.__func__(*args, **kwds)
|
||||
|
||||
@property
|
||||
def __doc__(self):
|
||||
opts_desc = _describe_option('all', _print_desc=False)
|
||||
opts_list = pp_options_list(list(_registered_options.keys()))
|
||||
return self.__doc_tmpl__.format(opts_desc=opts_desc,
|
||||
opts_list=opts_list)
|
||||
|
||||
|
||||
_get_option_tmpl = """
|
||||
get_option(pat)
|
||||
|
||||
Retrieves the value of the specified option.
|
||||
|
||||
Available options:
|
||||
|
||||
{opts_list}
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pat : str
|
||||
Regexp which should match a single option.
|
||||
Note: partial matches are supported for convenience, but unless you use the
|
||||
full option name (e.g. x.y.z.option_name), your code may break in future
|
||||
versions if new options with similar names are introduced.
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : the value of the option
|
||||
|
||||
Raises
|
||||
------
|
||||
OptionError : if no such option exists
|
||||
|
||||
Notes
|
||||
-----
|
||||
The available options with its descriptions:
|
||||
|
||||
{opts_desc}
|
||||
"""
|
||||
|
||||
_set_option_tmpl = """
|
||||
set_option(pat, value)
|
||||
|
||||
Sets the value of the specified option.
|
||||
|
||||
Available options:
|
||||
|
||||
{opts_list}
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pat : str
|
||||
Regexp which should match a single option.
|
||||
Note: partial matches are supported for convenience, but unless you use the
|
||||
full option name (e.g. x.y.z.option_name), your code may break in future
|
||||
versions if new options with similar names are introduced.
|
||||
value :
|
||||
new value of option.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
|
||||
Raises
|
||||
------
|
||||
OptionError if no such option exists
|
||||
|
||||
Notes
|
||||
-----
|
||||
The available options with its descriptions:
|
||||
|
||||
{opts_desc}
|
||||
"""
|
||||
|
||||
_describe_option_tmpl = """
|
||||
describe_option(pat, _print_desc=False)
|
||||
|
||||
Prints the description for one or more registered options.
|
||||
|
||||
Call with not arguments to get a listing for all registered options.
|
||||
|
||||
Available options:
|
||||
|
||||
{opts_list}
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pat : str
|
||||
Regexp pattern. All matching keys will have their description displayed.
|
||||
_print_desc : bool, default True
|
||||
If True (default) the description(s) will be printed to stdout.
|
||||
Otherwise, the description(s) will be returned as a unicode string
|
||||
(for testing).
|
||||
|
||||
Returns
|
||||
-------
|
||||
None by default, the description(s) as a unicode string if _print_desc
|
||||
is False
|
||||
|
||||
Notes
|
||||
-----
|
||||
The available options with its descriptions:
|
||||
|
||||
{opts_desc}
|
||||
"""
|
||||
|
||||
_reset_option_tmpl = """
|
||||
reset_option(pat)
|
||||
|
||||
Reset one or more options to their default value.
|
||||
|
||||
Pass "all" as argument to reset all options.
|
||||
|
||||
Available options:
|
||||
|
||||
{opts_list}
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pat : str/regex
|
||||
If specified only options matching `prefix*` will be reset.
|
||||
Note: partial matches are supported for convenience, but unless you
|
||||
use the full option name (e.g. x.y.z.option_name), your code may break
|
||||
in future versions if new options with similar names are introduced.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
|
||||
Notes
|
||||
-----
|
||||
The available options with its descriptions:
|
||||
|
||||
{opts_desc}
|
||||
"""
|
||||
|
||||
# bind the functions with their docstrings into a Callable
|
||||
# and use that as the functions exposed in pd.api
|
||||
get_option = CallableDynamicDoc(_get_option, _get_option_tmpl)
|
||||
set_option = CallableDynamicDoc(_set_option, _set_option_tmpl)
|
||||
reset_option = CallableDynamicDoc(_reset_option, _reset_option_tmpl)
|
||||
describe_option = CallableDynamicDoc(_describe_option, _describe_option_tmpl)
|
||||
options = DictWrapper(_global_config)
|
||||
|
||||
#
|
||||
# Functions for use by pandas developers, in addition to User - api
|
||||
|
||||
|
||||
class option_context(object):
|
||||
"""
|
||||
Context manager to temporarily set options in the `with` statement context.
|
||||
|
||||
You need to invoke as ``option_context(pat, val, [(pat, val), ...])``.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> with option_context('display.max_rows', 10, 'display.max_columns', 5):
|
||||
...
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args):
|
||||
if not (len(args) % 2 == 0 and len(args) >= 2):
|
||||
raise ValueError('Need to invoke as'
|
||||
'option_context(pat, val, [(pat, val), ...)).')
|
||||
|
||||
self.ops = list(zip(args[::2], args[1::2]))
|
||||
|
||||
def __enter__(self):
|
||||
undo = []
|
||||
for pat, val in self.ops:
|
||||
undo.append((pat, _get_option(pat, silent=True)))
|
||||
|
||||
self.undo = undo
|
||||
|
||||
for pat, val in self.ops:
|
||||
_set_option(pat, val, silent=True)
|
||||
|
||||
def __exit__(self, *args):
|
||||
if self.undo:
|
||||
for pat, val in self.undo:
|
||||
_set_option(pat, val, silent=True)
|
||||
|
||||
|
||||
def register_option(key, defval, doc='', validator=None, cb=None):
|
||||
"""Register an option in the package-wide pandas config object
|
||||
|
||||
Parameters
|
||||
----------
|
||||
key - a fully-qualified key, e.g. "x.y.option - z".
|
||||
defval - the default value of the option
|
||||
doc - a string description of the option
|
||||
validator - a function of a single argument, should raise `ValueError` if
|
||||
called with a value which is not a legal value for the option.
|
||||
cb - a function of a single argument "key", which is called
|
||||
immediately after an option value is set/reset. key is
|
||||
the full name of the option.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Nothing.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError if `validator` is specified and `defval` is not a valid value.
|
||||
|
||||
"""
|
||||
import tokenize
|
||||
import keyword
|
||||
key = key.lower()
|
||||
|
||||
if key in _registered_options:
|
||||
msg = "Option '{key}' has already been registered"
|
||||
raise OptionError(msg.format(key=key))
|
||||
if key in _reserved_keys:
|
||||
msg = "Option '{key}' is a reserved key"
|
||||
raise OptionError(msg.format(key=key))
|
||||
|
||||
# the default value should be legal
|
||||
if validator:
|
||||
validator(defval)
|
||||
|
||||
# walk the nested dict, creating dicts as needed along the path
|
||||
path = key.split('.')
|
||||
|
||||
for k in path:
|
||||
if not bool(re.match('^' + tokenize.Name + '$', k)):
|
||||
raise ValueError("{k} is not a valid identifier".format(k=k))
|
||||
if keyword.iskeyword(k):
|
||||
raise ValueError("{k} is a python keyword".format(k=k))
|
||||
|
||||
cursor = _global_config
|
||||
msg = "Path prefix to option '{option}' is already an option"
|
||||
for i, p in enumerate(path[:-1]):
|
||||
if not isinstance(cursor, dict):
|
||||
raise OptionError(msg.format(option='.'.join(path[:i])))
|
||||
if p not in cursor:
|
||||
cursor[p] = {}
|
||||
cursor = cursor[p]
|
||||
|
||||
if not isinstance(cursor, dict):
|
||||
raise OptionError(msg.format(option='.'.join(path[:-1])))
|
||||
|
||||
cursor[path[-1]] = defval # initialize
|
||||
|
||||
# save the option metadata
|
||||
_registered_options[key] = RegisteredOption(key=key, defval=defval,
|
||||
doc=doc, validator=validator,
|
||||
cb=cb)
|
||||
|
||||
|
||||
def deprecate_option(key, msg=None, rkey=None, removal_ver=None):
|
||||
"""
|
||||
Mark option `key` as deprecated, if code attempts to access this option,
|
||||
a warning will be produced, using `msg` if given, or a default message
|
||||
if not.
|
||||
if `rkey` is given, any access to the key will be re-routed to `rkey`.
|
||||
|
||||
Neither the existence of `key` nor that if `rkey` is checked. If they
|
||||
do not exist, any subsequence access will fail as usual, after the
|
||||
deprecation warning is given.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
key - the name of the option to be deprecated. must be a fully-qualified
|
||||
option name (e.g "x.y.z.rkey").
|
||||
|
||||
msg - (Optional) a warning message to output when the key is referenced.
|
||||
if no message is given a default message will be emitted.
|
||||
|
||||
rkey - (Optional) the name of an option to reroute access to.
|
||||
If specified, any referenced `key` will be re-routed to `rkey`
|
||||
including set/get/reset.
|
||||
rkey must be a fully-qualified option name (e.g "x.y.z.rkey").
|
||||
used by the default message if no `msg` is specified.
|
||||
|
||||
removal_ver - (Optional) specifies the version in which this option will
|
||||
be removed. used by the default message if no `msg`
|
||||
is specified.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Nothing
|
||||
|
||||
Raises
|
||||
------
|
||||
OptionError - if key has already been deprecated.
|
||||
|
||||
"""
|
||||
|
||||
key = key.lower()
|
||||
|
||||
if key in _deprecated_options:
|
||||
msg = "Option '{key}' has already been defined as deprecated."
|
||||
raise OptionError(msg.format(key=key))
|
||||
|
||||
_deprecated_options[key] = DeprecatedOption(key, msg, rkey, removal_ver)
|
||||
|
||||
#
|
||||
# functions internal to the module
|
||||
|
||||
|
||||
def _select_options(pat):
|
||||
"""returns a list of keys matching `pat`
|
||||
|
||||
if pat=="all", returns all registered options
|
||||
"""
|
||||
|
||||
# short-circuit for exact key
|
||||
if pat in _registered_options:
|
||||
return [pat]
|
||||
|
||||
# else look through all of them
|
||||
keys = sorted(_registered_options.keys())
|
||||
if pat == 'all': # reserved key
|
||||
return keys
|
||||
|
||||
return [k for k in keys if re.search(pat, k, re.I)]
|
||||
|
||||
|
||||
def _get_root(key):
|
||||
path = key.split('.')
|
||||
cursor = _global_config
|
||||
for p in path[:-1]:
|
||||
cursor = cursor[p]
|
||||
return cursor, path[-1]
|
||||
|
||||
|
||||
def _is_deprecated(key):
|
||||
""" Returns True if the given option has been deprecated """
|
||||
|
||||
key = key.lower()
|
||||
return key in _deprecated_options
|
||||
|
||||
|
||||
def _get_deprecated_option(key):
|
||||
"""
|
||||
Retrieves the metadata for a deprecated option, if `key` is deprecated.
|
||||
|
||||
Returns
|
||||
-------
|
||||
DeprecatedOption (namedtuple) if key is deprecated, None otherwise
|
||||
"""
|
||||
|
||||
try:
|
||||
d = _deprecated_options[key]
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return d
|
||||
|
||||
|
||||
def _get_registered_option(key):
|
||||
"""
|
||||
Retrieves the option metadata if `key` is a registered option.
|
||||
|
||||
Returns
|
||||
-------
|
||||
RegisteredOption (namedtuple) if key is deprecated, None otherwise
|
||||
"""
|
||||
return _registered_options.get(key)
|
||||
|
||||
|
||||
def _translate_key(key):
|
||||
"""
|
||||
if key id deprecated and a replacement key defined, will return the
|
||||
replacement key, otherwise returns `key` as - is
|
||||
"""
|
||||
|
||||
d = _get_deprecated_option(key)
|
||||
if d:
|
||||
return d.rkey or key
|
||||
else:
|
||||
return key
|
||||
|
||||
|
||||
def _warn_if_deprecated(key):
|
||||
"""
|
||||
Checks if `key` is a deprecated option and if so, prints a warning.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool - True if `key` is deprecated, False otherwise.
|
||||
"""
|
||||
|
||||
d = _get_deprecated_option(key)
|
||||
if d:
|
||||
if d.msg:
|
||||
print(d.msg)
|
||||
warnings.warn(d.msg, FutureWarning)
|
||||
else:
|
||||
msg = "'{key}' is deprecated".format(key=key)
|
||||
if d.removal_ver:
|
||||
msg += (' and will be removed in {version}'
|
||||
.format(version=d.removal_ver))
|
||||
if d.rkey:
|
||||
msg += ", please use '{rkey}' instead.".format(rkey=d.rkey)
|
||||
else:
|
||||
msg += ', please refrain from using it.'
|
||||
|
||||
warnings.warn(msg, FutureWarning)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _build_option_description(k):
|
||||
""" Builds a formatted description of a registered option and prints it """
|
||||
|
||||
o = _get_registered_option(k)
|
||||
d = _get_deprecated_option(k)
|
||||
|
||||
s = u('{k} ').format(k=k)
|
||||
|
||||
if o.doc:
|
||||
s += '\n'.join(o.doc.strip().split('\n'))
|
||||
else:
|
||||
s += 'No description available.'
|
||||
|
||||
if o:
|
||||
s += (u('\n [default: {default}] [currently: {current}]')
|
||||
.format(default=o.defval, current=_get_option(k, True)))
|
||||
|
||||
if d:
|
||||
s += u('\n (Deprecated')
|
||||
s += (u(', use `{rkey}` instead.')
|
||||
.format(rkey=d.rkey if d.rkey else ''))
|
||||
s += u(')')
|
||||
|
||||
s += '\n\n'
|
||||
return s
|
||||
|
||||
|
||||
def pp_options_list(keys, width=80, _print=False):
|
||||
""" Builds a concise listing of available options, grouped by prefix """
|
||||
|
||||
from textwrap import wrap
|
||||
from itertools import groupby
|
||||
|
||||
def pp(name, ks):
|
||||
pfx = ('- ' + name + '.[' if name else '')
|
||||
ls = wrap(', '.join(ks), width, initial_indent=pfx,
|
||||
subsequent_indent=' ', break_long_words=False)
|
||||
if ls and ls[-1] and name:
|
||||
ls[-1] = ls[-1] + ']'
|
||||
return ls
|
||||
|
||||
ls = []
|
||||
singles = [x for x in sorted(keys) if x.find('.') < 0]
|
||||
if singles:
|
||||
ls += pp('', singles)
|
||||
keys = [x for x in keys if x.find('.') >= 0]
|
||||
|
||||
for k, g in groupby(sorted(keys), lambda x: x[:x.rfind('.')]):
|
||||
ks = [x[len(k) + 1:] for x in list(g)]
|
||||
ls += pp(k, ks)
|
||||
s = '\n'.join(ls)
|
||||
if _print:
|
||||
print(s)
|
||||
else:
|
||||
return s
|
||||
|
||||
#
|
||||
# helpers
|
||||
|
||||
|
||||
@contextmanager
|
||||
def config_prefix(prefix):
|
||||
"""contextmanager for multiple invocations of API with a common prefix
|
||||
|
||||
supported API functions: (register / get / set )__option
|
||||
|
||||
Warning: This is not thread - safe, and won't work properly if you import
|
||||
the API functions into your module using the "from x import y" construct.
|
||||
|
||||
Example:
|
||||
|
||||
import pandas.core.config as cf
|
||||
with cf.config_prefix("display.font"):
|
||||
cf.register_option("color", "red")
|
||||
cf.register_option("size", " 5 pt")
|
||||
cf.set_option(size, " 6 pt")
|
||||
cf.get_option(size)
|
||||
...
|
||||
|
||||
etc'
|
||||
|
||||
will register options "display.font.color", "display.font.size", set the
|
||||
value of "display.font.size"... and so on.
|
||||
"""
|
||||
|
||||
# Note: reset_option relies on set_option, and on key directly
|
||||
# it does not fit in to this monkey-patching scheme
|
||||
|
||||
global register_option, get_option, set_option, reset_option
|
||||
|
||||
def wrap(func):
|
||||
def inner(key, *args, **kwds):
|
||||
pkey = '{prefix}.{key}'.format(prefix=prefix, key=key)
|
||||
return func(pkey, *args, **kwds)
|
||||
|
||||
return inner
|
||||
|
||||
_register_option = register_option
|
||||
_get_option = get_option
|
||||
_set_option = set_option
|
||||
set_option = wrap(set_option)
|
||||
get_option = wrap(get_option)
|
||||
register_option = wrap(register_option)
|
||||
yield None
|
||||
set_option = _set_option
|
||||
get_option = _get_option
|
||||
register_option = _register_option
|
||||
|
||||
# These factories and methods are handy for use as the validator
|
||||
# arg in register_option
|
||||
|
||||
|
||||
def is_type_factory(_type):
|
||||
"""
|
||||
|
||||
Parameters
|
||||
----------
|
||||
`_type` - a type to be compared against (e.g. type(x) == `_type`)
|
||||
|
||||
Returns
|
||||
-------
|
||||
validator - a function of a single argument x , which raises
|
||||
ValueError if type(x) is not equal to `_type`
|
||||
|
||||
"""
|
||||
|
||||
def inner(x):
|
||||
if type(x) != _type:
|
||||
msg = "Value must have type '{typ!s}'"
|
||||
raise ValueError(msg.format(typ=_type))
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
def is_instance_factory(_type):
|
||||
"""
|
||||
|
||||
Parameters
|
||||
----------
|
||||
`_type` - the type to be checked against
|
||||
|
||||
Returns
|
||||
-------
|
||||
validator - a function of a single argument x , which raises
|
||||
ValueError if x is not an instance of `_type`
|
||||
|
||||
"""
|
||||
if isinstance(_type, (tuple, list)):
|
||||
_type = tuple(_type)
|
||||
from pandas.io.formats.printing import pprint_thing
|
||||
type_repr = "|".join(map(pprint_thing, _type))
|
||||
else:
|
||||
type_repr = "'{typ}'".format(typ=_type)
|
||||
|
||||
def inner(x):
|
||||
if not isinstance(x, _type):
|
||||
msg = "Value must be an instance of {type_repr}"
|
||||
raise ValueError(msg.format(type_repr=type_repr))
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
def is_one_of_factory(legal_values):
|
||||
|
||||
callables = [c for c in legal_values if callable(c)]
|
||||
legal_values = [c for c in legal_values if not callable(c)]
|
||||
|
||||
def inner(x):
|
||||
from pandas.io.formats.printing import pprint_thing as pp
|
||||
if x not in legal_values:
|
||||
|
||||
if not any(c(x) for c in callables):
|
||||
pp_values = pp("|".join(lmap(pp, legal_values)))
|
||||
msg = "Value must be one of {pp_values}"
|
||||
if len(callables):
|
||||
msg += " or a callable"
|
||||
raise ValueError(msg.format(pp_values=pp_values))
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
# common type validators, for convenience
|
||||
# usage: register_option(... , validator = is_int)
|
||||
is_int = is_type_factory(int)
|
||||
is_bool = is_type_factory(bool)
|
||||
is_float = is_type_factory(float)
|
||||
is_str = is_type_factory(str)
|
||||
is_unicode = is_type_factory(compat.text_type)
|
||||
is_text = is_instance_factory((str, bytes))
|
||||
|
||||
|
||||
def is_callable(obj):
|
||||
"""
|
||||
|
||||
Parameters
|
||||
----------
|
||||
`obj` - the object to be checked
|
||||
|
||||
Returns
|
||||
-------
|
||||
validator - returns True if object is callable
|
||||
raises ValueError otherwise.
|
||||
|
||||
"""
|
||||
if not callable(obj):
|
||||
raise ValueError("Value must be a callable")
|
||||
return True
|
||||
@@ -0,0 +1,505 @@
|
||||
"""
|
||||
This module is imported from the pandas package __init__.py file
|
||||
in order to ensure that the core.config options registered here will
|
||||
be available as soon as the user loads the package. if register_option
|
||||
is invoked inside specific modules, they will not be registered until that
|
||||
module is imported, which may or may not be a problem.
|
||||
|
||||
If you need to make sure options are available even before a certain
|
||||
module is imported, register them here rather then in the module.
|
||||
|
||||
"""
|
||||
import pandas.core.config as cf
|
||||
from pandas.core.config import (is_int, is_bool, is_text, is_instance_factory,
|
||||
is_one_of_factory, is_callable)
|
||||
from pandas.io.formats.console import detect_console_encoding
|
||||
from pandas.io.formats.terminal import is_terminal
|
||||
|
||||
# compute
|
||||
|
||||
use_bottleneck_doc = """
|
||||
: bool
|
||||
Use the bottleneck library to accelerate if it is installed,
|
||||
the default is True
|
||||
Valid values: False,True
|
||||
"""
|
||||
|
||||
|
||||
def use_bottleneck_cb(key):
|
||||
from pandas.core import nanops
|
||||
nanops.set_use_bottleneck(cf.get_option(key))
|
||||
|
||||
|
||||
use_numexpr_doc = """
|
||||
: bool
|
||||
Use the numexpr library to accelerate computation if it is installed,
|
||||
the default is True
|
||||
Valid values: False,True
|
||||
"""
|
||||
|
||||
|
||||
def use_numexpr_cb(key):
|
||||
from pandas.core.computation import expressions
|
||||
expressions.set_use_numexpr(cf.get_option(key))
|
||||
|
||||
|
||||
with cf.config_prefix('compute'):
|
||||
cf.register_option('use_bottleneck', True, use_bottleneck_doc,
|
||||
validator=is_bool, cb=use_bottleneck_cb)
|
||||
cf.register_option('use_numexpr', True, use_numexpr_doc,
|
||||
validator=is_bool, cb=use_numexpr_cb)
|
||||
#
|
||||
# options from the "display" namespace
|
||||
|
||||
pc_precision_doc = """
|
||||
: int
|
||||
Floating point output precision (number of significant digits). This is
|
||||
only a suggestion
|
||||
"""
|
||||
|
||||
pc_colspace_doc = """
|
||||
: int
|
||||
Default space for DataFrame columns.
|
||||
"""
|
||||
|
||||
pc_max_rows_doc = """
|
||||
: int
|
||||
If max_rows is exceeded, switch to truncate view. Depending on
|
||||
`large_repr`, objects are either centrally truncated or printed as
|
||||
a summary view. 'None' value means unlimited.
|
||||
|
||||
In case python/IPython is running in a terminal and `large_repr`
|
||||
equals 'truncate' this can be set to 0 and pandas will auto-detect
|
||||
the height of the terminal and print a truncated object which fits
|
||||
the screen height. The IPython notebook, IPython qtconsole, or
|
||||
IDLE do not run in a terminal and hence it is not possible to do
|
||||
correct auto-detection.
|
||||
"""
|
||||
|
||||
pc_max_cols_doc = """
|
||||
: int
|
||||
If max_cols is exceeded, switch to truncate view. Depending on
|
||||
`large_repr`, objects are either centrally truncated or printed as
|
||||
a summary view. 'None' value means unlimited.
|
||||
|
||||
In case python/IPython is running in a terminal and `large_repr`
|
||||
equals 'truncate' this can be set to 0 and pandas will auto-detect
|
||||
the width of the terminal and print a truncated object which fits
|
||||
the screen width. The IPython notebook, IPython qtconsole, or IDLE
|
||||
do not run in a terminal and hence it is not possible to do
|
||||
correct auto-detection.
|
||||
"""
|
||||
|
||||
pc_max_categories_doc = """
|
||||
: int
|
||||
This sets the maximum number of categories pandas should output when
|
||||
printing out a `Categorical` or a Series of dtype "category".
|
||||
"""
|
||||
|
||||
pc_max_info_cols_doc = """
|
||||
: int
|
||||
max_info_columns is used in DataFrame.info method to decide if
|
||||
per column information will be printed.
|
||||
"""
|
||||
|
||||
pc_nb_repr_h_doc = """
|
||||
: boolean
|
||||
When True, IPython notebook will use html representation for
|
||||
pandas objects (if it is available).
|
||||
"""
|
||||
|
||||
pc_date_dayfirst_doc = """
|
||||
: boolean
|
||||
When True, prints and parses dates with the day first, eg 20/01/2005
|
||||
"""
|
||||
|
||||
pc_date_yearfirst_doc = """
|
||||
: boolean
|
||||
When True, prints and parses dates with the year first, eg 2005/01/20
|
||||
"""
|
||||
|
||||
pc_pprint_nest_depth = """
|
||||
: int
|
||||
Controls the number of nested levels to process when pretty-printing
|
||||
"""
|
||||
|
||||
pc_multi_sparse_doc = """
|
||||
: boolean
|
||||
"sparsify" MultiIndex display (don't display repeated
|
||||
elements in outer levels within groups)
|
||||
"""
|
||||
|
||||
pc_encoding_doc = """
|
||||
: str/unicode
|
||||
Defaults to the detected encoding of the console.
|
||||
Specifies the encoding to be used for strings returned by to_string,
|
||||
these are generally strings meant to be displayed on the console.
|
||||
"""
|
||||
|
||||
float_format_doc = """
|
||||
: callable
|
||||
The callable should accept a floating point number and return
|
||||
a string with the desired format of the number. This is used
|
||||
in some places like SeriesFormatter.
|
||||
See formats.format.EngFormatter for an example.
|
||||
"""
|
||||
|
||||
max_colwidth_doc = """
|
||||
: int
|
||||
The maximum width in characters of a column in the repr of
|
||||
a pandas data structure. When the column overflows, a "..."
|
||||
placeholder is embedded in the output.
|
||||
"""
|
||||
|
||||
colheader_justify_doc = """
|
||||
: 'left'/'right'
|
||||
Controls the justification of column headers. used by DataFrameFormatter.
|
||||
"""
|
||||
|
||||
pc_expand_repr_doc = """
|
||||
: boolean
|
||||
Whether to print out the full DataFrame repr for wide DataFrames across
|
||||
multiple lines, `max_columns` is still respected, but the output will
|
||||
wrap-around across multiple "pages" if its width exceeds `display.width`.
|
||||
"""
|
||||
|
||||
pc_show_dimensions_doc = """
|
||||
: boolean or 'truncate'
|
||||
Whether to print out dimensions at the end of DataFrame repr.
|
||||
If 'truncate' is specified, only print out the dimensions if the
|
||||
frame is truncated (e.g. not display all rows and/or columns)
|
||||
"""
|
||||
|
||||
pc_east_asian_width_doc = """
|
||||
: boolean
|
||||
Whether to use the Unicode East Asian Width to calculate the display text
|
||||
width.
|
||||
Enabling this may affect to the performance (default: False)
|
||||
"""
|
||||
|
||||
pc_ambiguous_as_wide_doc = """
|
||||
: boolean
|
||||
Whether to handle Unicode characters belong to Ambiguous as Wide (width=2)
|
||||
(default: False)
|
||||
"""
|
||||
|
||||
pc_latex_repr_doc = """
|
||||
: boolean
|
||||
Whether to produce a latex DataFrame representation for jupyter
|
||||
environments that support it.
|
||||
(default: False)
|
||||
"""
|
||||
|
||||
pc_table_schema_doc = """
|
||||
: boolean
|
||||
Whether to publish a Table Schema representation for frontends
|
||||
that support it.
|
||||
(default: False)
|
||||
"""
|
||||
|
||||
pc_html_border_doc = """
|
||||
: int
|
||||
A ``border=value`` attribute is inserted in the ``<table>`` tag
|
||||
for the DataFrame HTML repr.
|
||||
"""
|
||||
|
||||
pc_html_border_deprecation_warning = """\
|
||||
html.border has been deprecated, use display.html.border instead
|
||||
(currently both are identical)
|
||||
"""
|
||||
|
||||
pc_html_use_mathjax_doc = """\
|
||||
: boolean
|
||||
When True, Jupyter notebook will process table contents using MathJax,
|
||||
rendering mathematical expressions enclosed by the dollar symbol.
|
||||
(default: True)
|
||||
"""
|
||||
|
||||
pc_width_doc = """
|
||||
: int
|
||||
Width of the display in characters. In case python/IPython is running in
|
||||
a terminal this can be set to None and pandas will correctly auto-detect
|
||||
the width.
|
||||
Note that the IPython notebook, IPython qtconsole, or IDLE do not run in a
|
||||
terminal and hence it is not possible to correctly detect the width.
|
||||
"""
|
||||
|
||||
pc_chop_threshold_doc = """
|
||||
: float or None
|
||||
if set to a float value, all float values smaller then the given threshold
|
||||
will be displayed as exactly 0 by repr and friends.
|
||||
"""
|
||||
|
||||
pc_max_seq_items = """
|
||||
: int or None
|
||||
when pretty-printing a long sequence, no more then `max_seq_items`
|
||||
will be printed. If items are omitted, they will be denoted by the
|
||||
addition of "..." to the resulting string.
|
||||
|
||||
If set to None, the number of items to be printed is unlimited.
|
||||
"""
|
||||
|
||||
pc_max_info_rows_doc = """
|
||||
: int or None
|
||||
df.info() will usually show null-counts for each column.
|
||||
For large frames this can be quite slow. max_info_rows and max_info_cols
|
||||
limit this null check only to frames with smaller dimensions than
|
||||
specified.
|
||||
"""
|
||||
|
||||
pc_large_repr_doc = """
|
||||
: 'truncate'/'info'
|
||||
For DataFrames exceeding max_rows/max_cols, the repr (and HTML repr) can
|
||||
show a truncated table (the default from 0.13), or switch to the view from
|
||||
df.info() (the behaviour in earlier versions of pandas).
|
||||
"""
|
||||
|
||||
pc_memory_usage_doc = """
|
||||
: bool, string or None
|
||||
This specifies if the memory usage of a DataFrame should be displayed when
|
||||
df.info() is called. Valid values True,False,'deep'
|
||||
"""
|
||||
|
||||
pc_latex_escape = """
|
||||
: bool
|
||||
This specifies if the to_latex method of a Dataframe uses escapes special
|
||||
characters.
|
||||
Valid values: False,True
|
||||
"""
|
||||
|
||||
pc_latex_longtable = """
|
||||
:bool
|
||||
This specifies if the to_latex method of a Dataframe uses the longtable
|
||||
format.
|
||||
Valid values: False,True
|
||||
"""
|
||||
|
||||
pc_latex_multicolumn = """
|
||||
: bool
|
||||
This specifies if the to_latex method of a Dataframe uses multicolumns
|
||||
to pretty-print MultiIndex columns.
|
||||
Valid values: False,True
|
||||
"""
|
||||
|
||||
pc_latex_multicolumn_format = """
|
||||
: string
|
||||
This specifies the format for multicolumn headers.
|
||||
Can be surrounded with '|'.
|
||||
Valid values: 'l', 'c', 'r', 'p{<width>}'
|
||||
"""
|
||||
|
||||
pc_latex_multirow = """
|
||||
: bool
|
||||
This specifies if the to_latex method of a Dataframe uses multirows
|
||||
to pretty-print MultiIndex rows.
|
||||
Valid values: False,True
|
||||
"""
|
||||
|
||||
style_backup = dict()
|
||||
|
||||
|
||||
def table_schema_cb(key):
|
||||
from pandas.io.formats.printing import _enable_data_resource_formatter
|
||||
_enable_data_resource_formatter(cf.get_option(key))
|
||||
|
||||
|
||||
with cf.config_prefix('display'):
|
||||
cf.register_option('precision', 6, pc_precision_doc, validator=is_int)
|
||||
cf.register_option('float_format', None, float_format_doc,
|
||||
validator=is_one_of_factory([None, is_callable]))
|
||||
cf.register_option('column_space', 12, validator=is_int)
|
||||
cf.register_option('max_info_rows', 1690785, pc_max_info_rows_doc,
|
||||
validator=is_instance_factory((int, type(None))))
|
||||
cf.register_option('max_rows', 60, pc_max_rows_doc,
|
||||
validator=is_instance_factory([type(None), int]))
|
||||
cf.register_option('max_categories', 8, pc_max_categories_doc,
|
||||
validator=is_int)
|
||||
cf.register_option('max_colwidth', 50, max_colwidth_doc, validator=is_int)
|
||||
if is_terminal():
|
||||
max_cols = 0 # automatically determine optimal number of columns
|
||||
else:
|
||||
max_cols = 20 # cannot determine optimal number of columns
|
||||
cf.register_option('max_columns', max_cols, pc_max_cols_doc,
|
||||
validator=is_instance_factory([type(None), int]))
|
||||
cf.register_option('large_repr', 'truncate', pc_large_repr_doc,
|
||||
validator=is_one_of_factory(['truncate', 'info']))
|
||||
cf.register_option('max_info_columns', 100, pc_max_info_cols_doc,
|
||||
validator=is_int)
|
||||
cf.register_option('colheader_justify', 'right', colheader_justify_doc,
|
||||
validator=is_text)
|
||||
cf.register_option('notebook_repr_html', True, pc_nb_repr_h_doc,
|
||||
validator=is_bool)
|
||||
cf.register_option('date_dayfirst', False, pc_date_dayfirst_doc,
|
||||
validator=is_bool)
|
||||
cf.register_option('date_yearfirst', False, pc_date_yearfirst_doc,
|
||||
validator=is_bool)
|
||||
cf.register_option('pprint_nest_depth', 3, pc_pprint_nest_depth,
|
||||
validator=is_int)
|
||||
cf.register_option('multi_sparse', True, pc_multi_sparse_doc,
|
||||
validator=is_bool)
|
||||
cf.register_option('encoding', detect_console_encoding(), pc_encoding_doc,
|
||||
validator=is_text)
|
||||
cf.register_option('expand_frame_repr', True, pc_expand_repr_doc)
|
||||
cf.register_option('show_dimensions', 'truncate', pc_show_dimensions_doc,
|
||||
validator=is_one_of_factory([True, False, 'truncate']))
|
||||
cf.register_option('chop_threshold', None, pc_chop_threshold_doc)
|
||||
cf.register_option('max_seq_items', 100, pc_max_seq_items)
|
||||
cf.register_option('width', 80, pc_width_doc,
|
||||
validator=is_instance_factory([type(None), int]))
|
||||
cf.register_option('memory_usage', True, pc_memory_usage_doc,
|
||||
validator=is_one_of_factory([None, True,
|
||||
False, 'deep']))
|
||||
cf.register_option('unicode.east_asian_width', False,
|
||||
pc_east_asian_width_doc, validator=is_bool)
|
||||
cf.register_option('unicode.ambiguous_as_wide', False,
|
||||
pc_east_asian_width_doc, validator=is_bool)
|
||||
cf.register_option('latex.repr', False,
|
||||
pc_latex_repr_doc, validator=is_bool)
|
||||
cf.register_option('latex.escape', True, pc_latex_escape,
|
||||
validator=is_bool)
|
||||
cf.register_option('latex.longtable', False, pc_latex_longtable,
|
||||
validator=is_bool)
|
||||
cf.register_option('latex.multicolumn', True, pc_latex_multicolumn,
|
||||
validator=is_bool)
|
||||
cf.register_option('latex.multicolumn_format', 'l', pc_latex_multicolumn,
|
||||
validator=is_text)
|
||||
cf.register_option('latex.multirow', False, pc_latex_multirow,
|
||||
validator=is_bool)
|
||||
cf.register_option('html.table_schema', False, pc_table_schema_doc,
|
||||
validator=is_bool, cb=table_schema_cb)
|
||||
cf.register_option('html.border', 1, pc_html_border_doc,
|
||||
validator=is_int)
|
||||
cf.register_option('html.use_mathjax', True, pc_html_use_mathjax_doc,
|
||||
validator=is_bool)
|
||||
|
||||
with cf.config_prefix('html'):
|
||||
cf.register_option('border', 1, pc_html_border_doc,
|
||||
validator=is_int)
|
||||
|
||||
cf.deprecate_option('html.border', msg=pc_html_border_deprecation_warning,
|
||||
rkey='display.html.border')
|
||||
|
||||
|
||||
tc_sim_interactive_doc = """
|
||||
: boolean
|
||||
Whether to simulate interactive mode for purposes of testing
|
||||
"""
|
||||
|
||||
with cf.config_prefix('mode'):
|
||||
cf.register_option('sim_interactive', False, tc_sim_interactive_doc)
|
||||
|
||||
use_inf_as_null_doc = """
|
||||
: boolean
|
||||
use_inf_as_null had been deprecated and will be removed in a future
|
||||
version. Use `use_inf_as_na` instead.
|
||||
"""
|
||||
|
||||
use_inf_as_na_doc = """
|
||||
: boolean
|
||||
True means treat None, NaN, INF, -INF as NA (old way),
|
||||
False means None and NaN are null, but INF, -INF are not NA
|
||||
(new way).
|
||||
"""
|
||||
|
||||
# We don't want to start importing everything at the global context level
|
||||
# or we'll hit circular deps.
|
||||
|
||||
|
||||
def use_inf_as_na_cb(key):
|
||||
from pandas.core.dtypes.missing import _use_inf_as_na
|
||||
_use_inf_as_na(key)
|
||||
|
||||
|
||||
with cf.config_prefix('mode'):
|
||||
cf.register_option('use_inf_as_na', False, use_inf_as_na_doc,
|
||||
cb=use_inf_as_na_cb)
|
||||
cf.register_option('use_inf_as_null', False, use_inf_as_null_doc,
|
||||
cb=use_inf_as_na_cb)
|
||||
|
||||
cf.deprecate_option('mode.use_inf_as_null', msg=use_inf_as_null_doc,
|
||||
rkey='mode.use_inf_as_na')
|
||||
|
||||
|
||||
# user warnings
|
||||
chained_assignment = """
|
||||
: string
|
||||
Raise an exception, warn, or no action if trying to use chained assignment,
|
||||
The default is warn
|
||||
"""
|
||||
|
||||
with cf.config_prefix('mode'):
|
||||
cf.register_option('chained_assignment', 'warn', chained_assignment,
|
||||
validator=is_one_of_factory([None, 'warn', 'raise']))
|
||||
|
||||
# Set up the io.excel specific configuration.
|
||||
writer_engine_doc = """
|
||||
: string
|
||||
The default Excel writer engine for '{ext}' files. Available options:
|
||||
auto, {others}.
|
||||
"""
|
||||
|
||||
_xls_options = ['xlwt']
|
||||
_xlsm_options = ['openpyxl']
|
||||
_xlsx_options = ['openpyxl', 'xlsxwriter']
|
||||
|
||||
|
||||
with cf.config_prefix("io.excel.xls"):
|
||||
cf.register_option("writer", "auto",
|
||||
writer_engine_doc.format(
|
||||
ext='xls',
|
||||
others=', '.join(_xls_options)),
|
||||
validator=str)
|
||||
|
||||
with cf.config_prefix("io.excel.xlsm"):
|
||||
cf.register_option("writer", "auto",
|
||||
writer_engine_doc.format(
|
||||
ext='xlsm',
|
||||
others=', '.join(_xlsm_options)),
|
||||
validator=str)
|
||||
|
||||
|
||||
with cf.config_prefix("io.excel.xlsx"):
|
||||
cf.register_option("writer", "auto",
|
||||
writer_engine_doc.format(
|
||||
ext='xlsx',
|
||||
others=', '.join(_xlsx_options)),
|
||||
validator=str)
|
||||
|
||||
|
||||
# Set up the io.parquet specific configuration.
|
||||
parquet_engine_doc = """
|
||||
: string
|
||||
The default parquet reader/writer engine. Available options:
|
||||
'auto', 'pyarrow', 'fastparquet', the default is 'auto'
|
||||
"""
|
||||
|
||||
with cf.config_prefix('io.parquet'):
|
||||
cf.register_option(
|
||||
'engine', 'auto', parquet_engine_doc,
|
||||
validator=is_one_of_factory(['auto', 'pyarrow', 'fastparquet']))
|
||||
|
||||
# --------
|
||||
# Plotting
|
||||
# ---------
|
||||
|
||||
register_converter_doc = """
|
||||
: bool
|
||||
Whether to register converters with matplotlib's units registry for
|
||||
dates, times, datetimes, and Periods. Toggling to False will remove
|
||||
the converters, restoring any converters that pandas overwrote.
|
||||
"""
|
||||
|
||||
|
||||
def register_converter_cb(key):
|
||||
from pandas.plotting import register_matplotlib_converters
|
||||
from pandas.plotting import deregister_matplotlib_converters
|
||||
|
||||
if cf.get_option(key):
|
||||
register_matplotlib_converters()
|
||||
else:
|
||||
deregister_matplotlib_converters()
|
||||
|
||||
|
||||
with cf.config_prefix("plotting.matplotlib"):
|
||||
cf.register_option("register_converters", True, register_converter_doc,
|
||||
validator=bool, cb=register_converter_cb)
|
||||
@@ -0,0 +1,55 @@
|
||||
"""A collection of random tools for dealing with dates in Python.
|
||||
|
||||
.. deprecated:: 0.19.0
|
||||
Use pandas.tseries module instead.
|
||||
"""
|
||||
|
||||
# flake8: noqa
|
||||
|
||||
import warnings
|
||||
|
||||
from pandas.core.tools.datetimes import *
|
||||
from pandas.tseries.offsets import *
|
||||
from pandas.tseries.frequencies import *
|
||||
|
||||
warnings.warn("The pandas.core.datetools module is deprecated and will be "
|
||||
"removed in a future version. Please use the pandas.tseries "
|
||||
"module instead.", FutureWarning, stacklevel=2)
|
||||
|
||||
day = DateOffset()
|
||||
bday = BDay()
|
||||
businessDay = bday
|
||||
try:
|
||||
cday = CDay()
|
||||
customBusinessDay = CustomBusinessDay()
|
||||
customBusinessMonthEnd = CBMonthEnd()
|
||||
customBusinessMonthBegin = CBMonthBegin()
|
||||
except NotImplementedError:
|
||||
cday = None
|
||||
customBusinessDay = None
|
||||
customBusinessMonthEnd = None
|
||||
customBusinessMonthBegin = None
|
||||
monthEnd = MonthEnd()
|
||||
yearEnd = YearEnd()
|
||||
yearBegin = YearBegin()
|
||||
bmonthEnd = BMonthEnd()
|
||||
bmonthBegin = BMonthBegin()
|
||||
cbmonthEnd = customBusinessMonthEnd
|
||||
cbmonthBegin = customBusinessMonthBegin
|
||||
bquarterEnd = BQuarterEnd()
|
||||
quarterEnd = QuarterEnd()
|
||||
byearEnd = BYearEnd()
|
||||
week = Week()
|
||||
|
||||
# Functions/offsets to roll dates forward
|
||||
thisMonthEnd = MonthEnd(0)
|
||||
thisBMonthEnd = BMonthEnd(0)
|
||||
thisYearEnd = YearEnd(0)
|
||||
thisYearBegin = YearBegin(0)
|
||||
thisBQuarterEnd = BQuarterEnd(0)
|
||||
thisQuarterEnd = QuarterEnd(0)
|
||||
|
||||
# Functions to check where a date lies
|
||||
isBusinessDay = BDay().onOffset
|
||||
isMonthEnd = MonthEnd().onOffset
|
||||
isBMonthEnd = BMonthEnd().onOffset
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,82 @@
|
||||
# flake8: noqa
|
||||
|
||||
import sys
|
||||
|
||||
from .common import (pandas_dtype,
|
||||
is_dtype_equal,
|
||||
is_extension_type,
|
||||
|
||||
# categorical
|
||||
is_categorical,
|
||||
is_categorical_dtype,
|
||||
|
||||
# interval
|
||||
is_interval,
|
||||
is_interval_dtype,
|
||||
|
||||
# datetimelike
|
||||
is_datetimetz,
|
||||
is_datetime64_dtype,
|
||||
is_datetime64tz_dtype,
|
||||
is_datetime64_any_dtype,
|
||||
is_datetime64_ns_dtype,
|
||||
is_timedelta64_dtype,
|
||||
is_timedelta64_ns_dtype,
|
||||
is_period,
|
||||
is_period_dtype,
|
||||
|
||||
# string-like
|
||||
is_string_dtype,
|
||||
is_object_dtype,
|
||||
|
||||
# sparse
|
||||
is_sparse,
|
||||
|
||||
# numeric types
|
||||
is_scalar,
|
||||
is_sparse,
|
||||
is_bool,
|
||||
is_integer,
|
||||
is_float,
|
||||
is_complex,
|
||||
is_number,
|
||||
is_integer_dtype,
|
||||
is_int64_dtype,
|
||||
is_numeric_dtype,
|
||||
is_float_dtype,
|
||||
is_bool_dtype,
|
||||
is_complex_dtype,
|
||||
is_signed_integer_dtype,
|
||||
is_unsigned_integer_dtype,
|
||||
|
||||
# like
|
||||
is_re,
|
||||
is_re_compilable,
|
||||
is_dict_like,
|
||||
is_iterator,
|
||||
is_file_like,
|
||||
is_array_like,
|
||||
is_list_like,
|
||||
is_hashable,
|
||||
is_named_tuple)
|
||||
|
||||
|
||||
# deprecated
|
||||
m = sys.modules['pandas.core.dtypes.api']
|
||||
|
||||
for t in ['is_any_int_dtype', 'is_floating_dtype', 'is_sequence']:
|
||||
|
||||
def outer(t=t):
|
||||
|
||||
def wrapper(arr_or_dtype):
|
||||
import warnings
|
||||
import pandas
|
||||
warnings.warn("{t} is deprecated and will be "
|
||||
"removed in a future version".format(t=t),
|
||||
FutureWarning, stacklevel=3)
|
||||
return getattr(pandas.core.dtypes.common, t)(arr_or_dtype)
|
||||
return wrapper
|
||||
|
||||
setattr(m, t, outer(t))
|
||||
|
||||
del sys, m, t, outer
|
||||
@@ -0,0 +1,189 @@
|
||||
"""Extend pandas with custom array types"""
|
||||
import numpy as np
|
||||
|
||||
from pandas import compat
|
||||
from pandas.errors import AbstractMethodError
|
||||
|
||||
|
||||
class _DtypeOpsMixin(object):
|
||||
# Not all of pandas' extension dtypes are compatibile with
|
||||
# the new ExtensionArray interface. This means PandasExtensionDtype
|
||||
# can't subclass ExtensionDtype yet, as is_extension_array_dtype would
|
||||
# incorrectly say that these types are extension types.
|
||||
#
|
||||
# In the interim, we put methods that are shared between the two base
|
||||
# classes ExtensionDtype and PandasExtensionDtype here. Both those base
|
||||
# classes will inherit from this Mixin. Once everything is compatible, this
|
||||
# class's methods can be moved to ExtensionDtype and removed.
|
||||
|
||||
# na_value is the default NA value to use for this type. This is used in
|
||||
# e.g. ExtensionArray.take. This should be the user-facing "boxed" version
|
||||
# of the NA value, not the physical NA vaalue for storage.
|
||||
# e.g. for JSONArray, this is an empty dictionary.
|
||||
na_value = np.nan
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Check whether 'other' is equal to self.
|
||||
|
||||
By default, 'other' is considered equal if
|
||||
|
||||
* it's a string matching 'self.name'.
|
||||
* it's an instance of this type.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
other : Any
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
"""
|
||||
if isinstance(other, compat.string_types):
|
||||
return other == self.name
|
||||
elif isinstance(other, type(self)):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
@property
|
||||
def names(self):
|
||||
# type: () -> Optional[List[str]]
|
||||
"""Ordered list of field names, or None if there are no fields.
|
||||
|
||||
This is for compatibility with NumPy arrays, and may be removed in the
|
||||
future.
|
||||
"""
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def is_dtype(cls, dtype):
|
||||
"""Check if we match 'dtype'.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dtype : object
|
||||
The object to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
is_dtype : bool
|
||||
|
||||
Notes
|
||||
-----
|
||||
The default implementation is True if
|
||||
|
||||
1. ``cls.construct_from_string(dtype)`` is an instance
|
||||
of ``cls``.
|
||||
2. ``dtype`` is an object and is an instance of ``cls``
|
||||
3. ``dtype`` has a ``dtype`` attribute, and any of the above
|
||||
conditions is true for ``dtype.dtype``.
|
||||
"""
|
||||
dtype = getattr(dtype, 'dtype', dtype)
|
||||
|
||||
if isinstance(dtype, np.dtype):
|
||||
return False
|
||||
elif dtype is None:
|
||||
return False
|
||||
elif isinstance(dtype, cls):
|
||||
return True
|
||||
try:
|
||||
return cls.construct_from_string(dtype) is not None
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
|
||||
class ExtensionDtype(_DtypeOpsMixin):
|
||||
"""A custom data type, to be paired with an ExtensionArray.
|
||||
|
||||
.. versionadded:: 0.23.0
|
||||
|
||||
Notes
|
||||
-----
|
||||
The interface includes the following abstract methods that must
|
||||
be implemented by subclasses:
|
||||
|
||||
* type
|
||||
* name
|
||||
* construct_from_string
|
||||
|
||||
The `na_value` class attribute can be used to set the default NA value
|
||||
for this type. :attr:`numpy.nan` is used by default.
|
||||
|
||||
This class does not inherit from 'abc.ABCMeta' for performance reasons.
|
||||
Methods and properties required by the interface raise
|
||||
``pandas.errors.AbstractMethodError`` and no ``register`` method is
|
||||
provided for registering virtual subclasses.
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
# type: () -> type
|
||||
"""The scalar type for the array, e.g. ``int``
|
||||
|
||||
It's expected ``ExtensionArray[item]`` returns an instance
|
||||
of ``ExtensionDtype.type`` for scalar ``item``.
|
||||
"""
|
||||
raise AbstractMethodError(self)
|
||||
|
||||
@property
|
||||
def kind(self):
|
||||
# type () -> str
|
||||
"""A character code (one of 'biufcmMOSUV'), default 'O'
|
||||
|
||||
This should match the NumPy dtype used when the array is
|
||||
converted to an ndarray, which is probably 'O' for object if
|
||||
the extension type cannot be represented as a built-in NumPy
|
||||
type.
|
||||
|
||||
See Also
|
||||
--------
|
||||
numpy.dtype.kind
|
||||
"""
|
||||
return 'O'
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
# type: () -> str
|
||||
"""A string identifying the data type.
|
||||
|
||||
Will be used for display in, e.g. ``Series.dtype``
|
||||
"""
|
||||
raise AbstractMethodError(self)
|
||||
|
||||
@classmethod
|
||||
def construct_from_string(cls, string):
|
||||
"""Attempt to construct this type from a string.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
string : str
|
||||
|
||||
Returns
|
||||
-------
|
||||
self : instance of 'cls'
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
If a class cannot be constructed from this 'string'.
|
||||
|
||||
Examples
|
||||
--------
|
||||
If the extension dtype can be constructed without any arguments,
|
||||
the following may be an adequate implementation.
|
||||
|
||||
>>> @classmethod
|
||||
... def construct_from_string(cls, string)
|
||||
... if string == cls.name:
|
||||
... return cls()
|
||||
... else:
|
||||
... raise TypeError("Cannot construct a '{}' from "
|
||||
... "'{}'".format(cls, string))
|
||||
"""
|
||||
raise AbstractMethodError(cls)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,654 @@
|
||||
"""
|
||||
Utility functions related to concat
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pandas._libs.tslib as tslib
|
||||
from pandas import compat
|
||||
from pandas.core.dtypes.common import (
|
||||
is_categorical_dtype,
|
||||
is_sparse,
|
||||
is_extension_array_dtype,
|
||||
is_datetimetz,
|
||||
is_datetime64_dtype,
|
||||
is_timedelta64_dtype,
|
||||
is_period_dtype,
|
||||
is_object_dtype,
|
||||
is_bool_dtype,
|
||||
is_dtype_equal,
|
||||
_NS_DTYPE,
|
||||
_TD_DTYPE)
|
||||
from pandas.core.dtypes.generic import (
|
||||
ABCDatetimeIndex, ABCTimedeltaIndex,
|
||||
ABCPeriodIndex, ABCRangeIndex, ABCSparseDataFrame)
|
||||
|
||||
|
||||
def get_dtype_kinds(l):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
l : list of arrays
|
||||
|
||||
Returns
|
||||
-------
|
||||
a set of kinds that exist in this list of arrays
|
||||
"""
|
||||
|
||||
typs = set()
|
||||
for arr in l:
|
||||
|
||||
dtype = arr.dtype
|
||||
if is_categorical_dtype(dtype):
|
||||
typ = 'category'
|
||||
elif is_sparse(arr):
|
||||
typ = 'sparse'
|
||||
elif isinstance(arr, ABCRangeIndex):
|
||||
typ = 'range'
|
||||
elif is_datetimetz(arr):
|
||||
# if to_concat contains different tz,
|
||||
# the result must be object dtype
|
||||
typ = str(arr.dtype)
|
||||
elif is_datetime64_dtype(dtype):
|
||||
typ = 'datetime'
|
||||
elif is_timedelta64_dtype(dtype):
|
||||
typ = 'timedelta'
|
||||
elif is_object_dtype(dtype):
|
||||
typ = 'object'
|
||||
elif is_bool_dtype(dtype):
|
||||
typ = 'bool'
|
||||
elif is_period_dtype(dtype):
|
||||
typ = str(arr.dtype)
|
||||
else:
|
||||
typ = dtype.kind
|
||||
typs.add(typ)
|
||||
return typs
|
||||
|
||||
|
||||
def _get_series_result_type(result, objs=None):
|
||||
"""
|
||||
return appropriate class of Series concat
|
||||
input is either dict or array-like
|
||||
"""
|
||||
# concat Series with axis 1
|
||||
if isinstance(result, dict):
|
||||
# concat Series with axis 1
|
||||
if all(is_sparse(c) for c in compat.itervalues(result)):
|
||||
from pandas.core.sparse.api import SparseDataFrame
|
||||
return SparseDataFrame
|
||||
else:
|
||||
from pandas.core.frame import DataFrame
|
||||
return DataFrame
|
||||
|
||||
# otherwise it is a SingleBlockManager (axis = 0)
|
||||
if result._block.is_sparse:
|
||||
from pandas.core.sparse.api import SparseSeries
|
||||
return SparseSeries
|
||||
else:
|
||||
return objs[0]._constructor
|
||||
|
||||
|
||||
def _get_frame_result_type(result, objs):
|
||||
"""
|
||||
return appropriate class of DataFrame-like concat
|
||||
if all blocks are SparseBlock, return SparseDataFrame
|
||||
otherwise, return 1st obj
|
||||
"""
|
||||
|
||||
if result.blocks and all(b.is_sparse for b in result.blocks):
|
||||
from pandas.core.sparse.api import SparseDataFrame
|
||||
return SparseDataFrame
|
||||
else:
|
||||
return next(obj for obj in objs if not isinstance(obj,
|
||||
ABCSparseDataFrame))
|
||||
|
||||
|
||||
def _get_sliced_frame_result_type(data, obj):
|
||||
"""
|
||||
return appropriate class of Series. When data is sparse
|
||||
it will return a SparseSeries, otherwise it will return
|
||||
the Series.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : array-like
|
||||
obj : DataFrame
|
||||
|
||||
Returns
|
||||
-------
|
||||
Series or SparseSeries
|
||||
"""
|
||||
if is_sparse(data):
|
||||
from pandas.core.sparse.api import SparseSeries
|
||||
return SparseSeries
|
||||
return obj._constructor_sliced
|
||||
|
||||
|
||||
def _concat_compat(to_concat, axis=0):
|
||||
"""
|
||||
provide concatenation of an array of arrays each of which is a single
|
||||
'normalized' dtypes (in that for example, if it's object, then it is a
|
||||
non-datetimelike and provide a combined dtype for the resulting array that
|
||||
preserves the overall dtype if possible)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
to_concat : array of arrays
|
||||
axis : axis to provide concatenation
|
||||
|
||||
Returns
|
||||
-------
|
||||
a single array, preserving the combined dtypes
|
||||
"""
|
||||
|
||||
# filter empty arrays
|
||||
# 1-d dtypes always are included here
|
||||
def is_nonempty(x):
|
||||
try:
|
||||
return x.shape[axis] > 0
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
nonempty = [x for x in to_concat if is_nonempty(x)]
|
||||
|
||||
# If all arrays are empty, there's nothing to convert, just short-cut to
|
||||
# the concatenation, #3121.
|
||||
#
|
||||
# Creating an empty array directly is tempting, but the winnings would be
|
||||
# marginal given that it would still require shape & dtype calculation and
|
||||
# np.concatenate which has them both implemented is compiled.
|
||||
|
||||
typs = get_dtype_kinds(to_concat)
|
||||
|
||||
_contains_datetime = any(typ.startswith('datetime') for typ in typs)
|
||||
_contains_period = any(typ.startswith('period') for typ in typs)
|
||||
|
||||
if 'category' in typs:
|
||||
# this must be priort to _concat_datetime,
|
||||
# to support Categorical + datetime-like
|
||||
return _concat_categorical(to_concat, axis=axis)
|
||||
|
||||
elif _contains_datetime or 'timedelta' in typs or _contains_period:
|
||||
return _concat_datetime(to_concat, axis=axis, typs=typs)
|
||||
|
||||
# these are mandated to handle empties as well
|
||||
elif 'sparse' in typs:
|
||||
return _concat_sparse(to_concat, axis=axis, typs=typs)
|
||||
|
||||
extensions = [is_extension_array_dtype(x) for x in to_concat]
|
||||
if any(extensions) and axis == 1:
|
||||
to_concat = [np.atleast_2d(x.astype('object')) for x in to_concat]
|
||||
|
||||
if not nonempty:
|
||||
# we have all empties, but may need to coerce the result dtype to
|
||||
# object if we have non-numeric type operands (numpy would otherwise
|
||||
# cast this to float)
|
||||
typs = get_dtype_kinds(to_concat)
|
||||
if len(typs) != 1:
|
||||
|
||||
if (not len(typs - set(['i', 'u', 'f'])) or
|
||||
not len(typs - set(['bool', 'i', 'u']))):
|
||||
# let numpy coerce
|
||||
pass
|
||||
else:
|
||||
# coerce to object
|
||||
to_concat = [x.astype('object') for x in to_concat]
|
||||
|
||||
return np.concatenate(to_concat, axis=axis)
|
||||
|
||||
|
||||
def _concat_categorical(to_concat, axis=0):
|
||||
"""Concatenate an object/categorical array of arrays, each of which is a
|
||||
single dtype
|
||||
|
||||
Parameters
|
||||
----------
|
||||
to_concat : array of arrays
|
||||
axis : int
|
||||
Axis to provide concatenation in the current implementation this is
|
||||
always 0, e.g. we only have 1D categoricals
|
||||
|
||||
Returns
|
||||
-------
|
||||
Categorical
|
||||
A single array, preserving the combined dtypes
|
||||
"""
|
||||
|
||||
def _concat_asobject(to_concat):
|
||||
to_concat = [x.get_values() if is_categorical_dtype(x.dtype)
|
||||
else np.asarray(x).ravel() for x in to_concat]
|
||||
res = _concat_compat(to_concat)
|
||||
if axis == 1:
|
||||
return res.reshape(1, len(res))
|
||||
else:
|
||||
return res
|
||||
|
||||
# we could have object blocks and categoricals here
|
||||
# if we only have a single categoricals then combine everything
|
||||
# else its a non-compat categorical
|
||||
categoricals = [x for x in to_concat if is_categorical_dtype(x.dtype)]
|
||||
|
||||
# validate the categories
|
||||
if len(categoricals) != len(to_concat):
|
||||
pass
|
||||
else:
|
||||
# when all categories are identical
|
||||
first = to_concat[0]
|
||||
if all(first.is_dtype_equal(other) for other in to_concat[1:]):
|
||||
return union_categoricals(categoricals)
|
||||
|
||||
return _concat_asobject(to_concat)
|
||||
|
||||
|
||||
def union_categoricals(to_union, sort_categories=False, ignore_order=False):
|
||||
"""
|
||||
Combine list-like of Categorical-like, unioning categories. All
|
||||
categories must have the same dtype.
|
||||
|
||||
.. versionadded:: 0.19.0
|
||||
|
||||
Parameters
|
||||
----------
|
||||
to_union : list-like of Categorical, CategoricalIndex,
|
||||
or Series with dtype='category'
|
||||
sort_categories : boolean, default False
|
||||
If true, resulting categories will be lexsorted, otherwise
|
||||
they will be ordered as they appear in the data.
|
||||
ignore_order: boolean, default False
|
||||
If true, the ordered attribute of the Categoricals will be ignored.
|
||||
Results in an unordered categorical.
|
||||
|
||||
.. versionadded:: 0.20.0
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : Categorical
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
- all inputs do not have the same dtype
|
||||
- all inputs do not have the same ordered property
|
||||
- all inputs are ordered and their categories are not identical
|
||||
- sort_categories=True and Categoricals are ordered
|
||||
ValueError
|
||||
Empty list of categoricals passed
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
To learn more about categories, see `link
|
||||
<http://pandas.pydata.org/pandas-docs/stable/categorical.html#unioning>`__
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> from pandas.api.types import union_categoricals
|
||||
|
||||
If you want to combine categoricals that do not necessarily have
|
||||
the same categories, `union_categoricals` will combine a list-like
|
||||
of categoricals. The new categories will be the union of the
|
||||
categories being combined.
|
||||
|
||||
>>> a = pd.Categorical(["b", "c"])
|
||||
>>> b = pd.Categorical(["a", "b"])
|
||||
>>> union_categoricals([a, b])
|
||||
[b, c, a, b]
|
||||
Categories (3, object): [b, c, a]
|
||||
|
||||
By default, the resulting categories will be ordered as they appear
|
||||
in the `categories` of the data. If you want the categories to be
|
||||
lexsorted, use `sort_categories=True` argument.
|
||||
|
||||
>>> union_categoricals([a, b], sort_categories=True)
|
||||
[b, c, a, b]
|
||||
Categories (3, object): [a, b, c]
|
||||
|
||||
`union_categoricals` also works with the case of combining two
|
||||
categoricals of the same categories and order information (e.g. what
|
||||
you could also `append` for).
|
||||
|
||||
>>> a = pd.Categorical(["a", "b"], ordered=True)
|
||||
>>> b = pd.Categorical(["a", "b", "a"], ordered=True)
|
||||
>>> union_categoricals([a, b])
|
||||
[a, b, a, b, a]
|
||||
Categories (2, object): [a < b]
|
||||
|
||||
Raises `TypeError` because the categories are ordered and not identical.
|
||||
|
||||
>>> a = pd.Categorical(["a", "b"], ordered=True)
|
||||
>>> b = pd.Categorical(["a", "b", "c"], ordered=True)
|
||||
>>> union_categoricals([a, b])
|
||||
TypeError: to union ordered Categoricals, all categories must be the same
|
||||
|
||||
New in version 0.20.0
|
||||
|
||||
Ordered categoricals with different categories or orderings can be
|
||||
combined by using the `ignore_ordered=True` argument.
|
||||
|
||||
>>> a = pd.Categorical(["a", "b", "c"], ordered=True)
|
||||
>>> b = pd.Categorical(["c", "b", "a"], ordered=True)
|
||||
>>> union_categoricals([a, b], ignore_order=True)
|
||||
[a, b, c, c, b, a]
|
||||
Categories (3, object): [a, b, c]
|
||||
|
||||
`union_categoricals` also works with a `CategoricalIndex`, or `Series`
|
||||
containing categorical data, but note that the resulting array will
|
||||
always be a plain `Categorical`
|
||||
|
||||
>>> a = pd.Series(["b", "c"], dtype='category')
|
||||
>>> b = pd.Series(["a", "b"], dtype='category')
|
||||
>>> union_categoricals([a, b])
|
||||
[b, c, a, b]
|
||||
Categories (3, object): [b, c, a]
|
||||
"""
|
||||
from pandas import Index, Categorical, CategoricalIndex, Series
|
||||
from pandas.core.arrays.categorical import _recode_for_categories
|
||||
|
||||
if len(to_union) == 0:
|
||||
raise ValueError('No Categoricals to union')
|
||||
|
||||
def _maybe_unwrap(x):
|
||||
if isinstance(x, (CategoricalIndex, Series)):
|
||||
return x.values
|
||||
elif isinstance(x, Categorical):
|
||||
return x
|
||||
else:
|
||||
raise TypeError("all components to combine must be Categorical")
|
||||
|
||||
to_union = [_maybe_unwrap(x) for x in to_union]
|
||||
first = to_union[0]
|
||||
|
||||
if not all(is_dtype_equal(other.categories.dtype, first.categories.dtype)
|
||||
for other in to_union[1:]):
|
||||
raise TypeError("dtype of categories must be the same")
|
||||
|
||||
ordered = False
|
||||
if all(first.is_dtype_equal(other) for other in to_union[1:]):
|
||||
# identical categories - fastpath
|
||||
categories = first.categories
|
||||
ordered = first.ordered
|
||||
|
||||
if all(first.categories.equals(other.categories)
|
||||
for other in to_union[1:]):
|
||||
new_codes = np.concatenate([c.codes for c in to_union])
|
||||
else:
|
||||
codes = [first.codes] + [_recode_for_categories(other.codes,
|
||||
other.categories,
|
||||
first.categories)
|
||||
for other in to_union[1:]]
|
||||
new_codes = np.concatenate(codes)
|
||||
|
||||
if sort_categories and not ignore_order and ordered:
|
||||
raise TypeError("Cannot use sort_categories=True with "
|
||||
"ordered Categoricals")
|
||||
|
||||
if sort_categories and not categories.is_monotonic_increasing:
|
||||
categories = categories.sort_values()
|
||||
indexer = categories.get_indexer(first.categories)
|
||||
|
||||
from pandas.core.algorithms import take_1d
|
||||
new_codes = take_1d(indexer, new_codes, fill_value=-1)
|
||||
elif ignore_order or all(not c.ordered for c in to_union):
|
||||
# different categories - union and recode
|
||||
cats = first.categories.append([c.categories for c in to_union[1:]])
|
||||
categories = Index(cats.unique())
|
||||
if sort_categories:
|
||||
categories = categories.sort_values()
|
||||
|
||||
new_codes = []
|
||||
for c in to_union:
|
||||
new_codes.append(_recode_for_categories(c.codes, c.categories,
|
||||
categories))
|
||||
new_codes = np.concatenate(new_codes)
|
||||
else:
|
||||
# ordered - to show a proper error message
|
||||
if all(c.ordered for c in to_union):
|
||||
msg = ("to union ordered Categoricals, "
|
||||
"all categories must be the same")
|
||||
raise TypeError(msg)
|
||||
else:
|
||||
raise TypeError('Categorical.ordered must be the same')
|
||||
|
||||
if ignore_order:
|
||||
ordered = False
|
||||
|
||||
return Categorical(new_codes, categories=categories, ordered=ordered,
|
||||
fastpath=True)
|
||||
|
||||
|
||||
def _concatenate_2d(to_concat, axis):
|
||||
# coerce to 2d if needed & concatenate
|
||||
if axis == 1:
|
||||
to_concat = [np.atleast_2d(x) for x in to_concat]
|
||||
return np.concatenate(to_concat, axis=axis)
|
||||
|
||||
|
||||
def _concat_datetime(to_concat, axis=0, typs=None):
|
||||
"""
|
||||
provide concatenation of an datetimelike array of arrays each of which is a
|
||||
single M8[ns], datetimet64[ns, tz] or m8[ns] dtype
|
||||
|
||||
Parameters
|
||||
----------
|
||||
to_concat : array of arrays
|
||||
axis : axis to provide concatenation
|
||||
typs : set of to_concat dtypes
|
||||
|
||||
Returns
|
||||
-------
|
||||
a single array, preserving the combined dtypes
|
||||
"""
|
||||
|
||||
if typs is None:
|
||||
typs = get_dtype_kinds(to_concat)
|
||||
|
||||
# multiple types, need to coerce to object
|
||||
if len(typs) != 1:
|
||||
return _concatenate_2d([_convert_datetimelike_to_object(x)
|
||||
for x in to_concat],
|
||||
axis=axis)
|
||||
|
||||
# must be single dtype
|
||||
if any(typ.startswith('datetime') for typ in typs):
|
||||
|
||||
if 'datetime' in typs:
|
||||
to_concat = [np.array(x, copy=False).view(np.int64)
|
||||
for x in to_concat]
|
||||
return _concatenate_2d(to_concat, axis=axis).view(_NS_DTYPE)
|
||||
else:
|
||||
# when to_concat has different tz, len(typs) > 1.
|
||||
# thus no need to care
|
||||
return _concat_datetimetz(to_concat)
|
||||
|
||||
elif 'timedelta' in typs:
|
||||
return _concatenate_2d([x.view(np.int64) for x in to_concat],
|
||||
axis=axis).view(_TD_DTYPE)
|
||||
|
||||
elif any(typ.startswith('period') for typ in typs):
|
||||
# PeriodIndex must be handled by PeriodIndex,
|
||||
# Thus can't meet this condition ATM
|
||||
# Must be changed when we adding PeriodDtype
|
||||
raise NotImplementedError("unable to concat PeriodDtype")
|
||||
|
||||
|
||||
def _convert_datetimelike_to_object(x):
|
||||
# coerce datetimelike array to object dtype
|
||||
|
||||
# if dtype is of datetimetz or timezone
|
||||
if x.dtype.kind == _NS_DTYPE.kind:
|
||||
if getattr(x, 'tz', None) is not None:
|
||||
x = x.astype(object).values
|
||||
else:
|
||||
shape = x.shape
|
||||
x = tslib.ints_to_pydatetime(x.view(np.int64).ravel(),
|
||||
box="timestamp")
|
||||
x = x.reshape(shape)
|
||||
|
||||
elif x.dtype == _TD_DTYPE:
|
||||
shape = x.shape
|
||||
x = tslib.ints_to_pytimedelta(x.view(np.int64).ravel(), box=True)
|
||||
x = x.reshape(shape)
|
||||
|
||||
return x
|
||||
|
||||
|
||||
def _concat_datetimetz(to_concat, name=None):
|
||||
"""
|
||||
concat DatetimeIndex with the same tz
|
||||
all inputs must be DatetimeIndex
|
||||
it is used in DatetimeIndex.append also
|
||||
"""
|
||||
# do not pass tz to set because tzlocal cannot be hashed
|
||||
if len({str(x.dtype) for x in to_concat}) != 1:
|
||||
raise ValueError('to_concat must have the same tz')
|
||||
tz = to_concat[0].tz
|
||||
# no need to localize because internal repr will not be changed
|
||||
new_values = np.concatenate([x.asi8 for x in to_concat])
|
||||
return to_concat[0]._simple_new(new_values, tz=tz, name=name)
|
||||
|
||||
|
||||
def _concat_index_same_dtype(indexes, klass=None):
|
||||
klass = klass if klass is not None else indexes[0].__class__
|
||||
return klass(np.concatenate([x._values for x in indexes]))
|
||||
|
||||
|
||||
def _concat_index_asobject(to_concat, name=None):
|
||||
"""
|
||||
concat all inputs as object. DatetimeIndex, TimedeltaIndex and
|
||||
PeriodIndex are converted to object dtype before concatenation
|
||||
"""
|
||||
from pandas import Index
|
||||
from pandas.core.arrays import ExtensionArray
|
||||
|
||||
klasses = (ABCDatetimeIndex, ABCTimedeltaIndex, ABCPeriodIndex,
|
||||
ExtensionArray)
|
||||
to_concat = [x.astype(object) if isinstance(x, klasses) else x
|
||||
for x in to_concat]
|
||||
|
||||
self = to_concat[0]
|
||||
attribs = self._get_attributes_dict()
|
||||
attribs['name'] = name
|
||||
|
||||
to_concat = [x._values if isinstance(x, Index) else x
|
||||
for x in to_concat]
|
||||
return self._shallow_copy_with_infer(np.concatenate(to_concat), **attribs)
|
||||
|
||||
|
||||
def _concat_sparse(to_concat, axis=0, typs=None):
|
||||
"""
|
||||
provide concatenation of an sparse/dense array of arrays each of which is a
|
||||
single dtype
|
||||
|
||||
Parameters
|
||||
----------
|
||||
to_concat : array of arrays
|
||||
axis : axis to provide concatenation
|
||||
typs : set of to_concat dtypes
|
||||
|
||||
Returns
|
||||
-------
|
||||
a single array, preserving the combined dtypes
|
||||
"""
|
||||
|
||||
from pandas.core.sparse.array import SparseArray, _make_index
|
||||
|
||||
def convert_sparse(x, axis):
|
||||
# coerce to native type
|
||||
if isinstance(x, SparseArray):
|
||||
x = x.get_values()
|
||||
else:
|
||||
x = np.asarray(x)
|
||||
x = x.ravel()
|
||||
if axis > 0:
|
||||
x = np.atleast_2d(x)
|
||||
return x
|
||||
|
||||
if typs is None:
|
||||
typs = get_dtype_kinds(to_concat)
|
||||
|
||||
if len(typs) == 1:
|
||||
# concat input as it is if all inputs are sparse
|
||||
# and have the same fill_value
|
||||
fill_values = {c.fill_value for c in to_concat}
|
||||
if len(fill_values) == 1:
|
||||
sp_values = [c.sp_values for c in to_concat]
|
||||
indexes = [c.sp_index.to_int_index() for c in to_concat]
|
||||
|
||||
indices = []
|
||||
loc = 0
|
||||
for idx in indexes:
|
||||
indices.append(idx.indices + loc)
|
||||
loc += idx.length
|
||||
sp_values = np.concatenate(sp_values)
|
||||
indices = np.concatenate(indices)
|
||||
sp_index = _make_index(loc, indices, kind=to_concat[0].sp_index)
|
||||
|
||||
return SparseArray(sp_values, sparse_index=sp_index,
|
||||
fill_value=to_concat[0].fill_value)
|
||||
|
||||
# input may be sparse / dense mixed and may have different fill_value
|
||||
# input must contain sparse at least 1
|
||||
sparses = [c for c in to_concat if is_sparse(c)]
|
||||
fill_values = [c.fill_value for c in sparses]
|
||||
sp_indexes = [c.sp_index for c in sparses]
|
||||
|
||||
# densify and regular concat
|
||||
to_concat = [convert_sparse(x, axis) for x in to_concat]
|
||||
result = np.concatenate(to_concat, axis=axis)
|
||||
|
||||
if not len(typs - set(['sparse', 'f', 'i'])):
|
||||
# sparsify if inputs are sparse and dense numerics
|
||||
# first sparse input's fill_value and SparseIndex is used
|
||||
result = SparseArray(result.ravel(), fill_value=fill_values[0],
|
||||
kind=sp_indexes[0])
|
||||
else:
|
||||
# coerce to object if needed
|
||||
result = result.astype('object')
|
||||
return result
|
||||
|
||||
|
||||
def _concat_rangeindex_same_dtype(indexes):
|
||||
"""
|
||||
Concatenates multiple RangeIndex instances. All members of "indexes" must
|
||||
be of type RangeIndex; result will be RangeIndex if possible, Int64Index
|
||||
otherwise. E.g.:
|
||||
indexes = [RangeIndex(3), RangeIndex(3, 6)] -> RangeIndex(6)
|
||||
indexes = [RangeIndex(3), RangeIndex(4, 6)] -> Int64Index([0,1,2,4,5])
|
||||
"""
|
||||
from pandas import Int64Index, RangeIndex
|
||||
|
||||
start = step = next = None
|
||||
|
||||
# Filter the empty indexes
|
||||
non_empty_indexes = [obj for obj in indexes if len(obj)]
|
||||
|
||||
for obj in non_empty_indexes:
|
||||
|
||||
if start is None:
|
||||
# This is set by the first non-empty index
|
||||
start = obj._start
|
||||
if step is None and len(obj) > 1:
|
||||
step = obj._step
|
||||
elif step is None:
|
||||
# First non-empty index had only one element
|
||||
if obj._start == start:
|
||||
return _concat_index_same_dtype(indexes, klass=Int64Index)
|
||||
step = obj._start - start
|
||||
|
||||
non_consecutive = ((step != obj._step and len(obj) > 1) or
|
||||
(next is not None and obj._start != next))
|
||||
if non_consecutive:
|
||||
return _concat_index_same_dtype(indexes, klass=Int64Index)
|
||||
|
||||
if step is not None:
|
||||
next = obj[-1] + step
|
||||
|
||||
if non_empty_indexes:
|
||||
# Get the stop value from "next" or alternatively
|
||||
# from the last non-empty index
|
||||
stop = non_empty_indexes[-1]._stop if next is None else next
|
||||
return RangeIndex(start, stop, step)
|
||||
|
||||
# Here all "indexes" had 0 length, i.e. were empty.
|
||||
# In this case return an empty range index.
|
||||
return RangeIndex(0, 0)
|
||||
@@ -0,0 +1,725 @@
|
||||
""" define extension dtypes """
|
||||
|
||||
import re
|
||||
import numpy as np
|
||||
from pandas import compat
|
||||
from pandas.core.dtypes.generic import ABCIndexClass, ABCCategoricalIndex
|
||||
|
||||
from .base import ExtensionDtype, _DtypeOpsMixin
|
||||
|
||||
|
||||
class PandasExtensionDtype(_DtypeOpsMixin):
|
||||
"""
|
||||
A np.dtype duck-typed class, suitable for holding a custom dtype.
|
||||
|
||||
THIS IS NOT A REAL NUMPY DTYPE
|
||||
"""
|
||||
type = None
|
||||
subdtype = None
|
||||
kind = None
|
||||
str = None
|
||||
num = 100
|
||||
shape = tuple()
|
||||
itemsize = 8
|
||||
base = None
|
||||
isbuiltin = 0
|
||||
isnative = 0
|
||||
_metadata = []
|
||||
_cache = {}
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Return a string representation for a particular Object
|
||||
|
||||
Invoked by str(df) in both py2/py3.
|
||||
Yields Bytestring in Py2, Unicode String in py3.
|
||||
"""
|
||||
|
||||
if compat.PY3:
|
||||
return self.__unicode__()
|
||||
return self.__bytes__()
|
||||
|
||||
def __bytes__(self):
|
||||
"""
|
||||
Return a string representation for a particular object.
|
||||
|
||||
Invoked by bytes(obj) in py3 only.
|
||||
Yields a bytestring in both py2/py3.
|
||||
"""
|
||||
from pandas.core.config import get_option
|
||||
|
||||
encoding = get_option("display.encoding")
|
||||
return self.__unicode__().encode(encoding, 'replace')
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Return a string representation for a particular object.
|
||||
|
||||
Yields Bytestring in Py2, Unicode String in py3.
|
||||
"""
|
||||
return str(self)
|
||||
|
||||
def __hash__(self):
|
||||
raise NotImplementedError("sub-classes should implement an __hash__ "
|
||||
"method")
|
||||
|
||||
def __getstate__(self):
|
||||
# pickle support; we don't want to pickle the cache
|
||||
return {k: getattr(self, k, None) for k in self._metadata}
|
||||
|
||||
@classmethod
|
||||
def reset_cache(cls):
|
||||
""" clear the cache """
|
||||
cls._cache = {}
|
||||
|
||||
|
||||
class CategoricalDtypeType(type):
|
||||
"""
|
||||
the type of CategoricalDtype, this metaclass determines subclass ability
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class CategoricalDtype(PandasExtensionDtype, ExtensionDtype):
|
||||
"""
|
||||
Type for categorical data with the categories and orderedness
|
||||
|
||||
.. versionchanged:: 0.21.0
|
||||
|
||||
Parameters
|
||||
----------
|
||||
categories : sequence, optional
|
||||
Must be unique, and must not contain any nulls.
|
||||
ordered : bool, default False
|
||||
|
||||
Attributes
|
||||
----------
|
||||
categories
|
||||
ordered
|
||||
|
||||
Methods
|
||||
-------
|
||||
None
|
||||
|
||||
Notes
|
||||
-----
|
||||
This class is useful for specifying the type of a ``Categorical``
|
||||
independent of the values. See :ref:`categorical.categoricaldtype`
|
||||
for more.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> t = CategoricalDtype(categories=['b', 'a'], ordered=True)
|
||||
>>> pd.Series(['a', 'b', 'a', 'c'], dtype=t)
|
||||
0 a
|
||||
1 b
|
||||
2 a
|
||||
3 NaN
|
||||
dtype: category
|
||||
Categories (2, object): [b < a]
|
||||
|
||||
See Also
|
||||
--------
|
||||
pandas.Categorical
|
||||
"""
|
||||
# TODO: Document public vs. private API
|
||||
name = 'category'
|
||||
type = CategoricalDtypeType
|
||||
kind = 'O'
|
||||
str = '|O08'
|
||||
base = np.dtype('O')
|
||||
_metadata = ['categories', 'ordered']
|
||||
_cache = {}
|
||||
|
||||
def __init__(self, categories=None, ordered=None):
|
||||
self._finalize(categories, ordered, fastpath=False)
|
||||
|
||||
@classmethod
|
||||
def _from_fastpath(cls, categories=None, ordered=None):
|
||||
self = cls.__new__(cls)
|
||||
self._finalize(categories, ordered, fastpath=True)
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def _from_categorical_dtype(cls, dtype, categories=None, ordered=None):
|
||||
if categories is ordered is None:
|
||||
return dtype
|
||||
if categories is None:
|
||||
categories = dtype.categories
|
||||
if ordered is None:
|
||||
ordered = dtype.ordered
|
||||
return cls(categories, ordered)
|
||||
|
||||
def _finalize(self, categories, ordered, fastpath=False):
|
||||
|
||||
if ordered is not None:
|
||||
self.validate_ordered(ordered)
|
||||
|
||||
if categories is not None:
|
||||
categories = self.validate_categories(categories,
|
||||
fastpath=fastpath)
|
||||
|
||||
self._categories = categories
|
||||
self._ordered = ordered
|
||||
|
||||
def __setstate__(self, state):
|
||||
self._categories = state.pop('categories', None)
|
||||
self._ordered = state.pop('ordered', False)
|
||||
|
||||
def __hash__(self):
|
||||
# _hash_categories returns a uint64, so use the negative
|
||||
# space for when we have unknown categories to avoid a conflict
|
||||
if self.categories is None:
|
||||
if self.ordered:
|
||||
return -1
|
||||
else:
|
||||
return -2
|
||||
# We *do* want to include the real self.ordered here
|
||||
return int(self._hash_categories(self.categories, self.ordered))
|
||||
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
Rules for CDT equality:
|
||||
1) Any CDT is equal to the string 'category'
|
||||
2) Any CDT is equal to a CDT with categories=None regardless of ordered
|
||||
3) A CDT with ordered=True is only equal to another CDT with
|
||||
ordered=True and identical categories in the same order
|
||||
4) A CDT with ordered={False, None} is only equal to another CDT with
|
||||
ordered={False, None} and identical categories, but same order is
|
||||
not required. There is no distinction between False/None.
|
||||
5) Any other comparison returns False
|
||||
"""
|
||||
if isinstance(other, compat.string_types):
|
||||
return other == self.name
|
||||
|
||||
if not (hasattr(other, 'ordered') and hasattr(other, 'categories')):
|
||||
return False
|
||||
elif self.categories is None or other.categories is None:
|
||||
# We're forced into a suboptimal corner thanks to math and
|
||||
# backwards compatibility. We require that `CDT(...) == 'category'`
|
||||
# for all CDTs **including** `CDT(None, ...)`. Therefore, *all*
|
||||
# CDT(., .) = CDT(None, False) and *all*
|
||||
# CDT(., .) = CDT(None, True).
|
||||
return True
|
||||
elif self.ordered or other.ordered:
|
||||
# At least one has ordered=True; equal if both have ordered=True
|
||||
# and the same values for categories in the same order.
|
||||
return ((self.ordered == other.ordered) and
|
||||
self.categories.equals(other.categories))
|
||||
else:
|
||||
# Neither has ordered=True; equal if both have the same categories,
|
||||
# but same order is not necessary. There is no distinction between
|
||||
# ordered=False and ordered=None: CDT(., False) and CDT(., None)
|
||||
# will be equal if they have the same categories.
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __repr__(self):
|
||||
tpl = u'CategoricalDtype(categories={}ordered={})'
|
||||
if self.categories is None:
|
||||
data = u"None, "
|
||||
else:
|
||||
data = self.categories._format_data(name=self.__class__.__name__)
|
||||
return tpl.format(data, self.ordered)
|
||||
|
||||
@staticmethod
|
||||
def _hash_categories(categories, ordered=True):
|
||||
from pandas.core.util.hashing import (
|
||||
hash_array, _combine_hash_arrays, hash_tuples
|
||||
)
|
||||
|
||||
if len(categories) and isinstance(categories[0], tuple):
|
||||
# assumes if any individual category is a tuple, then all our. ATM
|
||||
# I don't really want to support just some of the categories being
|
||||
# tuples.
|
||||
categories = list(categories) # breaks if a np.array of categories
|
||||
cat_array = hash_tuples(categories)
|
||||
else:
|
||||
if categories.dtype == 'O':
|
||||
types = [type(x) for x in categories]
|
||||
if not len(set(types)) == 1:
|
||||
# TODO: hash_array doesn't handle mixed types. It casts
|
||||
# everything to a str first, which means we treat
|
||||
# {'1', '2'} the same as {'1', 2}
|
||||
# find a better solution
|
||||
cat_array = np.array([hash(x) for x in categories])
|
||||
hashed = hash((tuple(categories), ordered))
|
||||
return hashed
|
||||
cat_array = hash_array(np.asarray(categories), categorize=False)
|
||||
if ordered:
|
||||
cat_array = np.vstack([
|
||||
cat_array, np.arange(len(cat_array), dtype=cat_array.dtype)
|
||||
])
|
||||
else:
|
||||
cat_array = [cat_array]
|
||||
hashed = _combine_hash_arrays(iter(cat_array),
|
||||
num_items=len(cat_array))
|
||||
if len(hashed) == 0:
|
||||
# bug in Numpy<1.12 for length 0 arrays. Just return the correct
|
||||
# value of 0
|
||||
return 0
|
||||
else:
|
||||
return np.bitwise_xor.reduce(hashed)
|
||||
|
||||
@classmethod
|
||||
def construct_from_string(cls, string):
|
||||
""" attempt to construct this type from a string, raise a TypeError if
|
||||
it's not possible """
|
||||
try:
|
||||
if string == 'category':
|
||||
return cls()
|
||||
except:
|
||||
pass
|
||||
|
||||
raise TypeError("cannot construct a CategoricalDtype")
|
||||
|
||||
@staticmethod
|
||||
def validate_ordered(ordered):
|
||||
"""
|
||||
Validates that we have a valid ordered parameter. If
|
||||
it is not a boolean, a TypeError will be raised.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ordered : object
|
||||
The parameter to be verified.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
If 'ordered' is not a boolean.
|
||||
"""
|
||||
from pandas.core.dtypes.common import is_bool
|
||||
if not is_bool(ordered):
|
||||
raise TypeError("'ordered' must either be 'True' or 'False'")
|
||||
|
||||
@staticmethod
|
||||
def validate_categories(categories, fastpath=False):
|
||||
"""
|
||||
Validates that we have good categories
|
||||
|
||||
Parameters
|
||||
----------
|
||||
categories : array-like
|
||||
fastpath : bool
|
||||
Whether to skip nan and uniqueness checks
|
||||
|
||||
Returns
|
||||
-------
|
||||
categories : Index
|
||||
"""
|
||||
from pandas import Index
|
||||
|
||||
if not isinstance(categories, ABCIndexClass):
|
||||
categories = Index(categories, tupleize_cols=False)
|
||||
|
||||
if not fastpath:
|
||||
|
||||
if categories.hasnans:
|
||||
raise ValueError('Categorial categories cannot be null')
|
||||
|
||||
if not categories.is_unique:
|
||||
raise ValueError('Categorical categories must be unique')
|
||||
|
||||
if isinstance(categories, ABCCategoricalIndex):
|
||||
categories = categories.categories
|
||||
|
||||
return categories
|
||||
|
||||
def update_dtype(self, dtype):
|
||||
"""
|
||||
Returns a CategoricalDtype with categories and ordered taken from dtype
|
||||
if specified, otherwise falling back to self if unspecified
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dtype : CategoricalDtype
|
||||
|
||||
Returns
|
||||
-------
|
||||
new_dtype : CategoricalDtype
|
||||
"""
|
||||
if isinstance(dtype, compat.string_types) and dtype == 'category':
|
||||
# dtype='category' should not change anything
|
||||
return self
|
||||
elif not self.is_dtype(dtype):
|
||||
msg = ('a CategoricalDtype must be passed to perform an update, '
|
||||
'got {dtype!r}').format(dtype=dtype)
|
||||
raise ValueError(msg)
|
||||
|
||||
# dtype is CDT: keep current categories/ordered if None
|
||||
new_categories = dtype.categories
|
||||
if new_categories is None:
|
||||
new_categories = self.categories
|
||||
|
||||
new_ordered = dtype.ordered
|
||||
if new_ordered is None:
|
||||
new_ordered = self.ordered
|
||||
|
||||
return CategoricalDtype(new_categories, new_ordered)
|
||||
|
||||
@property
|
||||
def categories(self):
|
||||
"""
|
||||
An ``Index`` containing the unique categories allowed.
|
||||
"""
|
||||
return self._categories
|
||||
|
||||
@property
|
||||
def ordered(self):
|
||||
"""Whether the categories have an ordered relationship"""
|
||||
return self._ordered
|
||||
|
||||
|
||||
class DatetimeTZDtypeType(type):
|
||||
"""
|
||||
the type of DatetimeTZDtype, this metaclass determines subclass ability
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class DatetimeTZDtype(PandasExtensionDtype):
|
||||
|
||||
"""
|
||||
A np.dtype duck-typed class, suitable for holding a custom datetime with tz
|
||||
dtype.
|
||||
|
||||
THIS IS NOT A REAL NUMPY DTYPE, but essentially a sub-class of
|
||||
np.datetime64[ns]
|
||||
"""
|
||||
type = DatetimeTZDtypeType
|
||||
kind = 'M'
|
||||
str = '|M8[ns]'
|
||||
num = 101
|
||||
base = np.dtype('M8[ns]')
|
||||
_metadata = ['unit', 'tz']
|
||||
_match = re.compile(r"(datetime64|M8)\[(?P<unit>.+), (?P<tz>.+)\]")
|
||||
_cache = {}
|
||||
|
||||
def __new__(cls, unit=None, tz=None):
|
||||
""" Create a new unit if needed, otherwise return from the cache
|
||||
|
||||
Parameters
|
||||
----------
|
||||
unit : string unit that this represents, currently must be 'ns'
|
||||
tz : string tz that this represents
|
||||
"""
|
||||
|
||||
if isinstance(unit, DatetimeTZDtype):
|
||||
unit, tz = unit.unit, unit.tz
|
||||
|
||||
elif unit is None:
|
||||
# we are called as an empty constructor
|
||||
# generally for pickle compat
|
||||
return object.__new__(cls)
|
||||
|
||||
elif tz is None:
|
||||
|
||||
# we were passed a string that we can construct
|
||||
try:
|
||||
m = cls._match.search(unit)
|
||||
if m is not None:
|
||||
unit = m.groupdict()['unit']
|
||||
tz = m.groupdict()['tz']
|
||||
except:
|
||||
raise ValueError("could not construct DatetimeTZDtype")
|
||||
|
||||
elif isinstance(unit, compat.string_types):
|
||||
|
||||
if unit != 'ns':
|
||||
raise ValueError("DatetimeTZDtype only supports ns units")
|
||||
|
||||
unit = unit
|
||||
tz = tz
|
||||
|
||||
if tz is None:
|
||||
raise ValueError("DatetimeTZDtype constructor must have a tz "
|
||||
"supplied")
|
||||
|
||||
# hash with the actual tz if we can
|
||||
# some cannot be hashed, so stringfy
|
||||
try:
|
||||
key = (unit, tz)
|
||||
hash(key)
|
||||
except TypeError:
|
||||
key = (unit, str(tz))
|
||||
|
||||
# set/retrieve from cache
|
||||
try:
|
||||
return cls._cache[key]
|
||||
except KeyError:
|
||||
u = object.__new__(cls)
|
||||
u.unit = unit
|
||||
u.tz = tz
|
||||
cls._cache[key] = u
|
||||
return u
|
||||
|
||||
@classmethod
|
||||
def construct_from_string(cls, string):
|
||||
""" attempt to construct this type from a string, raise a TypeError if
|
||||
it's not possible
|
||||
"""
|
||||
try:
|
||||
return cls(unit=string)
|
||||
except ValueError:
|
||||
raise TypeError("could not construct DatetimeTZDtype")
|
||||
|
||||
def __unicode__(self):
|
||||
# format the tz
|
||||
return "datetime64[{unit}, {tz}]".format(unit=self.unit, tz=self.tz)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return str(self)
|
||||
|
||||
def __hash__(self):
|
||||
# make myself hashable
|
||||
return hash(str(self))
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, compat.string_types):
|
||||
return other == self.name
|
||||
|
||||
return (isinstance(other, DatetimeTZDtype) and
|
||||
self.unit == other.unit and
|
||||
str(self.tz) == str(other.tz))
|
||||
|
||||
|
||||
class PeriodDtypeType(type):
|
||||
"""
|
||||
the type of PeriodDtype, this metaclass determines subclass ability
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class PeriodDtype(PandasExtensionDtype):
|
||||
"""
|
||||
A Period duck-typed class, suitable for holding a period with freq dtype.
|
||||
|
||||
THIS IS NOT A REAL NUMPY DTYPE, but essentially a sub-class of np.int64.
|
||||
"""
|
||||
type = PeriodDtypeType
|
||||
kind = 'O'
|
||||
str = '|O08'
|
||||
base = np.dtype('O')
|
||||
num = 102
|
||||
_metadata = ['freq']
|
||||
_match = re.compile(r"(P|p)eriod\[(?P<freq>.+)\]")
|
||||
_cache = {}
|
||||
|
||||
def __new__(cls, freq=None):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
freq : frequency
|
||||
"""
|
||||
|
||||
if isinstance(freq, PeriodDtype):
|
||||
return freq
|
||||
|
||||
elif freq is None:
|
||||
# empty constructor for pickle compat
|
||||
return object.__new__(cls)
|
||||
|
||||
from pandas.tseries.offsets import DateOffset
|
||||
if not isinstance(freq, DateOffset):
|
||||
freq = cls._parse_dtype_strict(freq)
|
||||
|
||||
try:
|
||||
return cls._cache[freq.freqstr]
|
||||
except KeyError:
|
||||
u = object.__new__(cls)
|
||||
u.freq = freq
|
||||
cls._cache[freq.freqstr] = u
|
||||
return u
|
||||
|
||||
@classmethod
|
||||
def _parse_dtype_strict(cls, freq):
|
||||
if isinstance(freq, compat.string_types):
|
||||
if freq.startswith('period[') or freq.startswith('Period['):
|
||||
m = cls._match.search(freq)
|
||||
if m is not None:
|
||||
freq = m.group('freq')
|
||||
from pandas.tseries.frequencies import to_offset
|
||||
freq = to_offset(freq)
|
||||
if freq is not None:
|
||||
return freq
|
||||
|
||||
raise ValueError("could not construct PeriodDtype")
|
||||
|
||||
@classmethod
|
||||
def construct_from_string(cls, string):
|
||||
"""
|
||||
attempt to construct this type from a string, raise a TypeError
|
||||
if its not possible
|
||||
"""
|
||||
from pandas.tseries.offsets import DateOffset
|
||||
if isinstance(string, (compat.string_types, DateOffset)):
|
||||
# avoid tuple to be regarded as freq
|
||||
try:
|
||||
return cls(freq=string)
|
||||
except ValueError:
|
||||
pass
|
||||
raise TypeError("could not construct PeriodDtype")
|
||||
|
||||
def __unicode__(self):
|
||||
return "period[{freq}]".format(freq=self.freq.freqstr)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return str(self)
|
||||
|
||||
def __hash__(self):
|
||||
# make myself hashable
|
||||
return hash(str(self))
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, compat.string_types):
|
||||
return other == self.name or other == self.name.title()
|
||||
|
||||
return isinstance(other, PeriodDtype) and self.freq == other.freq
|
||||
|
||||
@classmethod
|
||||
def is_dtype(cls, dtype):
|
||||
"""
|
||||
Return a boolean if we if the passed type is an actual dtype that we
|
||||
can match (via string or type)
|
||||
"""
|
||||
|
||||
if isinstance(dtype, compat.string_types):
|
||||
# PeriodDtype can be instantiated from freq string like "U",
|
||||
# but doesn't regard freq str like "U" as dtype.
|
||||
if dtype.startswith('period[') or dtype.startswith('Period['):
|
||||
try:
|
||||
if cls._parse_dtype_strict(dtype) is not None:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except ValueError:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
return super(PeriodDtype, cls).is_dtype(dtype)
|
||||
|
||||
|
||||
class IntervalDtypeType(type):
|
||||
"""
|
||||
the type of IntervalDtype, this metaclass determines subclass ability
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class IntervalDtype(PandasExtensionDtype):
|
||||
"""
|
||||
A Interval duck-typed class, suitable for holding an interval
|
||||
|
||||
THIS IS NOT A REAL NUMPY DTYPE
|
||||
"""
|
||||
name = 'interval'
|
||||
type = IntervalDtypeType
|
||||
kind = None
|
||||
str = '|O08'
|
||||
base = np.dtype('O')
|
||||
num = 103
|
||||
_metadata = ['subtype']
|
||||
_match = re.compile(r"(I|i)nterval\[(?P<subtype>.+)\]")
|
||||
_cache = {}
|
||||
|
||||
def __new__(cls, subtype=None):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
subtype : the dtype of the Interval
|
||||
"""
|
||||
from pandas.core.dtypes.common import (
|
||||
is_categorical_dtype, is_string_dtype, pandas_dtype)
|
||||
|
||||
if isinstance(subtype, IntervalDtype):
|
||||
return subtype
|
||||
elif subtype is None:
|
||||
# we are called as an empty constructor
|
||||
# generally for pickle compat
|
||||
u = object.__new__(cls)
|
||||
u.subtype = None
|
||||
return u
|
||||
elif (isinstance(subtype, compat.string_types) and
|
||||
subtype.lower() == 'interval'):
|
||||
subtype = None
|
||||
else:
|
||||
if isinstance(subtype, compat.string_types):
|
||||
m = cls._match.search(subtype)
|
||||
if m is not None:
|
||||
subtype = m.group('subtype')
|
||||
|
||||
try:
|
||||
subtype = pandas_dtype(subtype)
|
||||
except TypeError:
|
||||
raise ValueError("could not construct IntervalDtype")
|
||||
|
||||
if is_categorical_dtype(subtype) or is_string_dtype(subtype):
|
||||
# GH 19016
|
||||
msg = ('category, object, and string subtypes are not supported '
|
||||
'for IntervalDtype')
|
||||
raise TypeError(msg)
|
||||
|
||||
try:
|
||||
return cls._cache[str(subtype)]
|
||||
except KeyError:
|
||||
u = object.__new__(cls)
|
||||
u.subtype = subtype
|
||||
cls._cache[str(subtype)] = u
|
||||
return u
|
||||
|
||||
@classmethod
|
||||
def construct_from_string(cls, string):
|
||||
"""
|
||||
attempt to construct this type from a string, raise a TypeError
|
||||
if its not possible
|
||||
"""
|
||||
if isinstance(string, compat.string_types):
|
||||
return cls(string)
|
||||
msg = "a string needs to be passed, got type {typ}"
|
||||
raise TypeError(msg.format(typ=type(string)))
|
||||
|
||||
def __unicode__(self):
|
||||
if self.subtype is None:
|
||||
return "interval"
|
||||
return "interval[{subtype}]".format(subtype=self.subtype)
|
||||
|
||||
def __hash__(self):
|
||||
# make myself hashable
|
||||
return hash(str(self))
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, compat.string_types):
|
||||
return other.lower() in (self.name.lower(), str(self).lower())
|
||||
elif not isinstance(other, IntervalDtype):
|
||||
return False
|
||||
elif self.subtype is None or other.subtype is None:
|
||||
# None should match any subtype
|
||||
return True
|
||||
else:
|
||||
from pandas.core.dtypes.common import is_dtype_equal
|
||||
return is_dtype_equal(self.subtype, other.subtype)
|
||||
|
||||
@classmethod
|
||||
def is_dtype(cls, dtype):
|
||||
"""
|
||||
Return a boolean if we if the passed type is an actual dtype that we
|
||||
can match (via string or type)
|
||||
"""
|
||||
|
||||
if isinstance(dtype, compat.string_types):
|
||||
if dtype.lower().startswith('interval'):
|
||||
try:
|
||||
if cls.construct_from_string(dtype) is not None:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except ValueError:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
return super(IntervalDtype, cls).is_dtype(dtype)
|
||||
@@ -0,0 +1,70 @@
|
||||
""" define generic base classes for pandas objects """
|
||||
|
||||
|
||||
# define abstract base classes to enable isinstance type checking on our
|
||||
# objects
|
||||
def create_pandas_abc_type(name, attr, comp):
|
||||
@classmethod
|
||||
def _check(cls, inst):
|
||||
return getattr(inst, attr, '_typ') in comp
|
||||
|
||||
dct = dict(__instancecheck__=_check, __subclasscheck__=_check)
|
||||
meta = type("ABCBase", (type, ), dct)
|
||||
return meta(name, tuple(), dct)
|
||||
|
||||
|
||||
ABCIndex = create_pandas_abc_type("ABCIndex", "_typ", ("index", ))
|
||||
ABCInt64Index = create_pandas_abc_type("ABCInt64Index", "_typ",
|
||||
("int64index", ))
|
||||
ABCUInt64Index = create_pandas_abc_type("ABCUInt64Index", "_typ",
|
||||
("uint64index", ))
|
||||
ABCRangeIndex = create_pandas_abc_type("ABCRangeIndex", "_typ",
|
||||
("rangeindex", ))
|
||||
ABCFloat64Index = create_pandas_abc_type("ABCFloat64Index", "_typ",
|
||||
("float64index", ))
|
||||
ABCMultiIndex = create_pandas_abc_type("ABCMultiIndex", "_typ",
|
||||
("multiindex", ))
|
||||
ABCDatetimeIndex = create_pandas_abc_type("ABCDatetimeIndex", "_typ",
|
||||
("datetimeindex", ))
|
||||
ABCTimedeltaIndex = create_pandas_abc_type("ABCTimedeltaIndex", "_typ",
|
||||
("timedeltaindex", ))
|
||||
ABCPeriodIndex = create_pandas_abc_type("ABCPeriodIndex", "_typ",
|
||||
("periodindex", ))
|
||||
ABCCategoricalIndex = create_pandas_abc_type("ABCCategoricalIndex", "_typ",
|
||||
("categoricalindex", ))
|
||||
ABCIntervalIndex = create_pandas_abc_type("ABCIntervalIndex", "_typ",
|
||||
("intervalindex", ))
|
||||
ABCIndexClass = create_pandas_abc_type("ABCIndexClass", "_typ",
|
||||
("index", "int64index", "rangeindex",
|
||||
"float64index", "uint64index",
|
||||
"multiindex", "datetimeindex",
|
||||
"timedeltaindex", "periodindex",
|
||||
"categoricalindex", "intervalindex"))
|
||||
|
||||
ABCSeries = create_pandas_abc_type("ABCSeries", "_typ", ("series", ))
|
||||
ABCDataFrame = create_pandas_abc_type("ABCDataFrame", "_typ", ("dataframe", ))
|
||||
ABCSparseDataFrame = create_pandas_abc_type("ABCSparseDataFrame", "_subtyp",
|
||||
("sparse_frame", ))
|
||||
ABCPanel = create_pandas_abc_type("ABCPanel", "_typ", ("panel",))
|
||||
ABCSparseSeries = create_pandas_abc_type("ABCSparseSeries", "_subtyp",
|
||||
('sparse_series',
|
||||
'sparse_time_series'))
|
||||
ABCSparseArray = create_pandas_abc_type("ABCSparseArray", "_subtyp",
|
||||
('sparse_array', 'sparse_series'))
|
||||
ABCCategorical = create_pandas_abc_type("ABCCategorical", "_typ",
|
||||
("categorical"))
|
||||
ABCPeriod = create_pandas_abc_type("ABCPeriod", "_typ", ("period", ))
|
||||
ABCDateOffset = create_pandas_abc_type("ABCDateOffset", "_typ",
|
||||
("dateoffset",))
|
||||
ABCInterval = create_pandas_abc_type("ABCInterval", "_typ", ("interval", ))
|
||||
ABCExtensionArray = create_pandas_abc_type("ABCExtensionArray", "_typ",
|
||||
("extension", "categorical",))
|
||||
|
||||
|
||||
class _ABCGeneric(type):
|
||||
|
||||
def __instancecheck__(cls, inst):
|
||||
return hasattr(inst, "_data")
|
||||
|
||||
|
||||
ABCGeneric = _ABCGeneric("ABCGeneric", tuple(), {})
|
||||
@@ -0,0 +1,476 @@
|
||||
""" basic inference routines """
|
||||
|
||||
import collections
|
||||
import re
|
||||
import numpy as np
|
||||
from collections import Iterable
|
||||
from numbers import Number
|
||||
from pandas.compat import (PY2, string_types, text_type,
|
||||
string_and_binary_types, re_type)
|
||||
from pandas._libs import lib
|
||||
|
||||
is_bool = lib.is_bool
|
||||
|
||||
is_integer = lib.is_integer
|
||||
|
||||
is_float = lib.is_float
|
||||
|
||||
is_complex = lib.is_complex
|
||||
|
||||
is_scalar = lib.is_scalar
|
||||
|
||||
is_decimal = lib.is_decimal
|
||||
|
||||
is_interval = lib.is_interval
|
||||
|
||||
|
||||
def is_number(obj):
|
||||
"""
|
||||
Check if the object is a number.
|
||||
|
||||
Returns True when the object is a number, and False if is not.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : any type
|
||||
The object to check if is a number.
|
||||
|
||||
Returns
|
||||
-------
|
||||
is_number : bool
|
||||
Whether `obj` is a number or not.
|
||||
|
||||
See Also
|
||||
--------
|
||||
pandas.api.types.is_integer: checks a subgroup of numbers
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> pd.api.types.is_number(1)
|
||||
True
|
||||
>>> pd.api.types.is_number(7.15)
|
||||
True
|
||||
|
||||
Booleans are valid because they are int subclass.
|
||||
|
||||
>>> pd.api.types.is_number(False)
|
||||
True
|
||||
|
||||
>>> pd.api.types.is_number("foo")
|
||||
False
|
||||
>>> pd.api.types.is_number("5")
|
||||
False
|
||||
"""
|
||||
|
||||
return isinstance(obj, (Number, np.number))
|
||||
|
||||
|
||||
def is_string_like(obj):
|
||||
"""
|
||||
Check if the object is a string.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : The object to check.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> is_string_like("foo")
|
||||
True
|
||||
>>> is_string_like(1)
|
||||
False
|
||||
|
||||
Returns
|
||||
-------
|
||||
is_str_like : bool
|
||||
Whether `obj` is a string or not.
|
||||
"""
|
||||
|
||||
return isinstance(obj, (text_type, string_types))
|
||||
|
||||
|
||||
def _iterable_not_string(obj):
|
||||
"""
|
||||
Check if the object is an iterable but not a string.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : The object to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
is_iter_not_string : bool
|
||||
Whether `obj` is a non-string iterable.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> _iterable_not_string([1, 2, 3])
|
||||
True
|
||||
>>> _iterable_not_string("foo")
|
||||
False
|
||||
>>> _iterable_not_string(1)
|
||||
False
|
||||
"""
|
||||
|
||||
return (isinstance(obj, collections.Iterable) and
|
||||
not isinstance(obj, string_types))
|
||||
|
||||
|
||||
def is_iterator(obj):
|
||||
"""
|
||||
Check if the object is an iterator.
|
||||
|
||||
For example, lists are considered iterators
|
||||
but not strings or datetime objects.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : The object to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
is_iter : bool
|
||||
Whether `obj` is an iterator.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> is_iterator([1, 2, 3])
|
||||
True
|
||||
>>> is_iterator(datetime(2017, 1, 1))
|
||||
False
|
||||
>>> is_iterator("foo")
|
||||
False
|
||||
>>> is_iterator(1)
|
||||
False
|
||||
"""
|
||||
|
||||
if not hasattr(obj, '__iter__'):
|
||||
return False
|
||||
|
||||
if PY2:
|
||||
return hasattr(obj, 'next')
|
||||
else:
|
||||
# Python 3 generators have
|
||||
# __next__ instead of next
|
||||
return hasattr(obj, '__next__')
|
||||
|
||||
|
||||
def is_file_like(obj):
|
||||
"""
|
||||
Check if the object is a file-like object.
|
||||
|
||||
For objects to be considered file-like, they must
|
||||
be an iterator AND have either a `read` and/or `write`
|
||||
method as an attribute.
|
||||
|
||||
Note: file-like objects must be iterable, but
|
||||
iterable objects need not be file-like.
|
||||
|
||||
.. versionadded:: 0.20.0
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : The object to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
is_file_like : bool
|
||||
Whether `obj` has file-like properties.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> buffer(StringIO("data"))
|
||||
>>> is_file_like(buffer)
|
||||
True
|
||||
>>> is_file_like([1, 2, 3])
|
||||
False
|
||||
"""
|
||||
|
||||
if not (hasattr(obj, 'read') or hasattr(obj, 'write')):
|
||||
return False
|
||||
|
||||
if not hasattr(obj, "__iter__"):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def is_re(obj):
|
||||
"""
|
||||
Check if the object is a regex pattern instance.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : The object to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
is_regex : bool
|
||||
Whether `obj` is a regex pattern.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> is_re(re.compile(".*"))
|
||||
True
|
||||
>>> is_re("foo")
|
||||
False
|
||||
"""
|
||||
|
||||
return isinstance(obj, re_type)
|
||||
|
||||
|
||||
def is_re_compilable(obj):
|
||||
"""
|
||||
Check if the object can be compiled into a regex pattern instance.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : The object to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
is_regex_compilable : bool
|
||||
Whether `obj` can be compiled as a regex pattern.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> is_re_compilable(".*")
|
||||
True
|
||||
>>> is_re_compilable(1)
|
||||
False
|
||||
"""
|
||||
|
||||
try:
|
||||
re.compile(obj)
|
||||
except TypeError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def is_list_like(obj):
|
||||
"""
|
||||
Check if the object is list-like.
|
||||
|
||||
Objects that are considered list-like are for example Python
|
||||
lists, tuples, sets, NumPy arrays, and Pandas Series.
|
||||
|
||||
Strings and datetime objects, however, are not considered list-like.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : The object to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
is_list_like : bool
|
||||
Whether `obj` has list-like properties.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> is_list_like([1, 2, 3])
|
||||
True
|
||||
>>> is_list_like({1, 2, 3})
|
||||
True
|
||||
>>> is_list_like(datetime(2017, 1, 1))
|
||||
False
|
||||
>>> is_list_like("foo")
|
||||
False
|
||||
>>> is_list_like(1)
|
||||
False
|
||||
"""
|
||||
|
||||
return (isinstance(obj, Iterable) and
|
||||
not isinstance(obj, string_and_binary_types))
|
||||
|
||||
|
||||
def is_array_like(obj):
|
||||
"""
|
||||
Check if the object is array-like.
|
||||
|
||||
For an object to be considered array-like, it must be list-like and
|
||||
have a `dtype` attribute.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : The object to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
is_array_like : bool
|
||||
Whether `obj` has array-like properties.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> is_array_like(np.array([1, 2, 3]))
|
||||
True
|
||||
>>> is_array_like(pd.Series(["a", "b"]))
|
||||
True
|
||||
>>> is_array_like(pd.Index(["2016-01-01"]))
|
||||
True
|
||||
>>> is_array_like([1, 2, 3])
|
||||
False
|
||||
>>> is_array_like(("a", "b"))
|
||||
False
|
||||
"""
|
||||
|
||||
return is_list_like(obj) and hasattr(obj, "dtype")
|
||||
|
||||
|
||||
def is_nested_list_like(obj):
|
||||
"""
|
||||
Check if the object is list-like, and that all of its elements
|
||||
are also list-like.
|
||||
|
||||
.. versionadded:: 0.20.0
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : The object to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
is_list_like : bool
|
||||
Whether `obj` has list-like properties.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> is_nested_list_like([[1, 2, 3]])
|
||||
True
|
||||
>>> is_nested_list_like([{1, 2, 3}, {1, 2, 3}])
|
||||
True
|
||||
>>> is_nested_list_like(["foo"])
|
||||
False
|
||||
>>> is_nested_list_like([])
|
||||
False
|
||||
>>> is_nested_list_like([[1, 2, 3], 1])
|
||||
False
|
||||
|
||||
Notes
|
||||
-----
|
||||
This won't reliably detect whether a consumable iterator (e. g.
|
||||
a generator) is a nested-list-like without consuming the iterator.
|
||||
To avoid consuming it, we always return False if the outer container
|
||||
doesn't define `__len__`.
|
||||
|
||||
See Also
|
||||
--------
|
||||
is_list_like
|
||||
"""
|
||||
return (is_list_like(obj) and hasattr(obj, '__len__') and
|
||||
len(obj) > 0 and all(is_list_like(item) for item in obj))
|
||||
|
||||
|
||||
def is_dict_like(obj):
|
||||
"""
|
||||
Check if the object is dict-like.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : The object to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
is_dict_like : bool
|
||||
Whether `obj` has dict-like properties.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> is_dict_like({1: 2})
|
||||
True
|
||||
>>> is_dict_like([1, 2, 3])
|
||||
False
|
||||
"""
|
||||
|
||||
return hasattr(obj, '__getitem__') and hasattr(obj, 'keys')
|
||||
|
||||
|
||||
def is_named_tuple(obj):
|
||||
"""
|
||||
Check if the object is a named tuple.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : The object to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
is_named_tuple : bool
|
||||
Whether `obj` is a named tuple.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> Point = namedtuple("Point", ["x", "y"])
|
||||
>>> p = Point(1, 2)
|
||||
>>>
|
||||
>>> is_named_tuple(p)
|
||||
True
|
||||
>>> is_named_tuple((1, 2))
|
||||
False
|
||||
"""
|
||||
|
||||
return isinstance(obj, tuple) and hasattr(obj, '_fields')
|
||||
|
||||
|
||||
def is_hashable(obj):
|
||||
"""Return True if hash(obj) will succeed, False otherwise.
|
||||
|
||||
Some types will pass a test against collections.Hashable but fail when they
|
||||
are actually hashed with hash().
|
||||
|
||||
Distinguish between these and other types by trying the call to hash() and
|
||||
seeing if they raise TypeError.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> a = ([],)
|
||||
>>> isinstance(a, collections.Hashable)
|
||||
True
|
||||
>>> is_hashable(a)
|
||||
False
|
||||
"""
|
||||
# Unfortunately, we can't use isinstance(obj, collections.Hashable), which
|
||||
# can be faster than calling hash. That is because numpy scalars on Python
|
||||
# 3 fail this test.
|
||||
|
||||
# Reconsider this decision once this numpy bug is fixed:
|
||||
# https://github.com/numpy/numpy/issues/5562
|
||||
|
||||
try:
|
||||
hash(obj)
|
||||
except TypeError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def is_sequence(obj):
|
||||
"""
|
||||
Check if the object is a sequence of objects.
|
||||
String types are not included as sequences here.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : The object to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
is_sequence : bool
|
||||
Whether `obj` is a sequence of objects.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> l = [1, 2, 3]
|
||||
>>>
|
||||
>>> is_sequence(l)
|
||||
True
|
||||
>>> is_sequence(iter(l))
|
||||
False
|
||||
"""
|
||||
|
||||
try:
|
||||
iter(obj) # Can iterate over it.
|
||||
len(obj) # Has a length associated with it.
|
||||
return not isinstance(obj, string_and_binary_types)
|
||||
except (TypeError, AttributeError):
|
||||
return False
|
||||
@@ -0,0 +1,532 @@
|
||||
"""
|
||||
missing types & inference
|
||||
"""
|
||||
import numpy as np
|
||||
from pandas._libs import lib, missing as libmissing
|
||||
from pandas._libs.tslib import NaT, iNaT
|
||||
from .generic import (ABCMultiIndex, ABCSeries,
|
||||
ABCIndexClass, ABCGeneric,
|
||||
ABCExtensionArray)
|
||||
from .common import (is_string_dtype, is_datetimelike,
|
||||
is_datetimelike_v_numeric, is_float_dtype,
|
||||
is_datetime64_dtype, is_datetime64tz_dtype,
|
||||
is_timedelta64_dtype, is_interval_dtype,
|
||||
is_period_dtype,
|
||||
is_complex_dtype,
|
||||
is_string_like_dtype, is_bool_dtype,
|
||||
is_integer_dtype, is_dtype_equal,
|
||||
is_extension_array_dtype,
|
||||
needs_i8_conversion, _ensure_object,
|
||||
pandas_dtype,
|
||||
is_scalar,
|
||||
is_object_dtype,
|
||||
is_integer,
|
||||
_TD_DTYPE,
|
||||
_NS_DTYPE)
|
||||
from .inference import is_list_like
|
||||
|
||||
isposinf_scalar = libmissing.isposinf_scalar
|
||||
isneginf_scalar = libmissing.isneginf_scalar
|
||||
|
||||
|
||||
def isna(obj):
|
||||
"""
|
||||
Detect missing values for an array-like object.
|
||||
|
||||
This function takes a scalar or array-like object and indictates
|
||||
whether values are missing (``NaN`` in numeric arrays, ``None`` or ``NaN``
|
||||
in object arrays, ``NaT`` in datetimelike).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : scalar or array-like
|
||||
Object to check for null or missing values.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool or array-like of bool
|
||||
For scalar input, returns a scalar boolean.
|
||||
For array input, returns an array of boolean indicating whether each
|
||||
corresponding element is missing.
|
||||
|
||||
See Also
|
||||
--------
|
||||
notna : boolean inverse of pandas.isna.
|
||||
Series.isna : Detetct missing values in a Series.
|
||||
DataFrame.isna : Detect missing values in a DataFrame.
|
||||
Index.isna : Detect missing values in an Index.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Scalar arguments (including strings) result in a scalar boolean.
|
||||
|
||||
>>> pd.isna('dog')
|
||||
False
|
||||
|
||||
>>> pd.isna(np.nan)
|
||||
True
|
||||
|
||||
ndarrays result in an ndarray of booleans.
|
||||
|
||||
>>> array = np.array([[1, np.nan, 3], [4, 5, np.nan]])
|
||||
>>> array
|
||||
array([[ 1., nan, 3.],
|
||||
[ 4., 5., nan]])
|
||||
>>> pd.isna(array)
|
||||
array([[False, True, False],
|
||||
[False, False, True]])
|
||||
|
||||
For indexes, an ndarray of booleans is returned.
|
||||
|
||||
>>> index = pd.DatetimeIndex(["2017-07-05", "2017-07-06", None,
|
||||
... "2017-07-08"])
|
||||
>>> index
|
||||
DatetimeIndex(['2017-07-05', '2017-07-06', 'NaT', '2017-07-08'],
|
||||
dtype='datetime64[ns]', freq=None)
|
||||
>>> pd.isna(index)
|
||||
array([False, False, True, False])
|
||||
|
||||
For Series and DataFrame, the same type is returned, containing booleans.
|
||||
|
||||
>>> df = pd.DataFrame([['ant', 'bee', 'cat'], ['dog', None, 'fly']])
|
||||
>>> df
|
||||
0 1 2
|
||||
0 ant bee cat
|
||||
1 dog None fly
|
||||
>>> pd.isna(df)
|
||||
0 1 2
|
||||
0 False False False
|
||||
1 False True False
|
||||
|
||||
>>> pd.isna(df[1])
|
||||
0 False
|
||||
1 True
|
||||
Name: 1, dtype: bool
|
||||
"""
|
||||
return _isna(obj)
|
||||
|
||||
|
||||
isnull = isna
|
||||
|
||||
|
||||
def _isna_new(obj):
|
||||
if is_scalar(obj):
|
||||
return libmissing.checknull(obj)
|
||||
# hack (for now) because MI registers as ndarray
|
||||
elif isinstance(obj, ABCMultiIndex):
|
||||
raise NotImplementedError("isna is not defined for MultiIndex")
|
||||
elif isinstance(obj, (ABCSeries, np.ndarray, ABCIndexClass,
|
||||
ABCExtensionArray)):
|
||||
return _isna_ndarraylike(obj)
|
||||
elif isinstance(obj, ABCGeneric):
|
||||
return obj._constructor(obj._data.isna(func=isna))
|
||||
elif isinstance(obj, list):
|
||||
return _isna_ndarraylike(np.asarray(obj, dtype=object))
|
||||
elif hasattr(obj, '__array__'):
|
||||
return _isna_ndarraylike(np.asarray(obj))
|
||||
else:
|
||||
return obj is None
|
||||
|
||||
|
||||
def _isna_old(obj):
|
||||
"""Detect missing values. Treat None, NaN, INF, -INF as null.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arr: ndarray or object value
|
||||
|
||||
Returns
|
||||
-------
|
||||
boolean ndarray or boolean
|
||||
"""
|
||||
if is_scalar(obj):
|
||||
return libmissing.checknull_old(obj)
|
||||
# hack (for now) because MI registers as ndarray
|
||||
elif isinstance(obj, ABCMultiIndex):
|
||||
raise NotImplementedError("isna is not defined for MultiIndex")
|
||||
elif isinstance(obj, (ABCSeries, np.ndarray, ABCIndexClass)):
|
||||
return _isna_ndarraylike_old(obj)
|
||||
elif isinstance(obj, ABCGeneric):
|
||||
return obj._constructor(obj._data.isna(func=_isna_old))
|
||||
elif isinstance(obj, list):
|
||||
return _isna_ndarraylike_old(np.asarray(obj, dtype=object))
|
||||
elif hasattr(obj, '__array__'):
|
||||
return _isna_ndarraylike_old(np.asarray(obj))
|
||||
else:
|
||||
return obj is None
|
||||
|
||||
|
||||
_isna = _isna_new
|
||||
|
||||
|
||||
def _use_inf_as_na(key):
|
||||
"""Option change callback for na/inf behaviour
|
||||
Choose which replacement for numpy.isnan / -numpy.isfinite is used.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
flag: bool
|
||||
True means treat None, NaN, INF, -INF as null (old way),
|
||||
False means None and NaN are null, but INF, -INF are not null
|
||||
(new way).
|
||||
|
||||
Notes
|
||||
-----
|
||||
This approach to setting global module values is discussed and
|
||||
approved here:
|
||||
|
||||
* http://stackoverflow.com/questions/4859217/
|
||||
programmatically-creating-variables-in-python/4859312#4859312
|
||||
"""
|
||||
from pandas.core.config import get_option
|
||||
flag = get_option(key)
|
||||
if flag:
|
||||
globals()['_isna'] = _isna_old
|
||||
else:
|
||||
globals()['_isna'] = _isna_new
|
||||
|
||||
|
||||
def _isna_ndarraylike(obj):
|
||||
values = getattr(obj, 'values', obj)
|
||||
dtype = values.dtype
|
||||
|
||||
if is_extension_array_dtype(obj):
|
||||
if isinstance(obj, (ABCIndexClass, ABCSeries)):
|
||||
values = obj._values
|
||||
else:
|
||||
values = obj
|
||||
result = values.isna()
|
||||
elif is_interval_dtype(values):
|
||||
# TODO(IntervalArray): remove this if block
|
||||
from pandas import IntervalIndex
|
||||
result = IntervalIndex(obj).isna()
|
||||
elif is_string_dtype(dtype):
|
||||
# Working around NumPy ticket 1542
|
||||
shape = values.shape
|
||||
|
||||
if is_string_like_dtype(dtype):
|
||||
# object array of strings
|
||||
result = np.zeros(values.shape, dtype=bool)
|
||||
else:
|
||||
# object array of non-strings
|
||||
result = np.empty(shape, dtype=bool)
|
||||
vec = libmissing.isnaobj(values.ravel())
|
||||
result[...] = vec.reshape(shape)
|
||||
|
||||
elif needs_i8_conversion(obj):
|
||||
# this is the NaT pattern
|
||||
result = values.view('i8') == iNaT
|
||||
else:
|
||||
result = np.isnan(values)
|
||||
|
||||
# box
|
||||
if isinstance(obj, ABCSeries):
|
||||
from pandas import Series
|
||||
result = Series(result, index=obj.index, name=obj.name, copy=False)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _isna_ndarraylike_old(obj):
|
||||
values = getattr(obj, 'values', obj)
|
||||
dtype = values.dtype
|
||||
|
||||
if is_string_dtype(dtype):
|
||||
# Working around NumPy ticket 1542
|
||||
shape = values.shape
|
||||
|
||||
if is_string_like_dtype(dtype):
|
||||
result = np.zeros(values.shape, dtype=bool)
|
||||
else:
|
||||
result = np.empty(shape, dtype=bool)
|
||||
vec = libmissing.isnaobj_old(values.ravel())
|
||||
result[:] = vec.reshape(shape)
|
||||
|
||||
elif is_datetime64_dtype(dtype):
|
||||
# this is the NaT pattern
|
||||
result = values.view('i8') == iNaT
|
||||
else:
|
||||
result = ~np.isfinite(values)
|
||||
|
||||
# box
|
||||
if isinstance(obj, ABCSeries):
|
||||
from pandas import Series
|
||||
result = Series(result, index=obj.index, name=obj.name, copy=False)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def notna(obj):
|
||||
"""
|
||||
Detect non-missing values for an array-like object.
|
||||
|
||||
This function takes a scalar or array-like object and indictates
|
||||
whether values are valid (not missing, which is ``NaN`` in numeric
|
||||
arrays, ``None`` or ``NaN`` in object arrays, ``NaT`` in datetimelike).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : array-like or object value
|
||||
Object to check for *not* null or *non*-missing values.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool or array-like of bool
|
||||
For scalar input, returns a scalar boolean.
|
||||
For array input, returns an array of boolean indicating whether each
|
||||
corresponding element is valid.
|
||||
|
||||
See Also
|
||||
--------
|
||||
isna : boolean inverse of pandas.notna.
|
||||
Series.notna : Detetct valid values in a Series.
|
||||
DataFrame.notna : Detect valid values in a DataFrame.
|
||||
Index.notna : Detect valid values in an Index.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Scalar arguments (including strings) result in a scalar boolean.
|
||||
|
||||
>>> pd.notna('dog')
|
||||
True
|
||||
|
||||
>>> pd.notna(np.nan)
|
||||
False
|
||||
|
||||
ndarrays result in an ndarray of booleans.
|
||||
|
||||
>>> array = np.array([[1, np.nan, 3], [4, 5, np.nan]])
|
||||
>>> array
|
||||
array([[ 1., nan, 3.],
|
||||
[ 4., 5., nan]])
|
||||
>>> pd.notna(array)
|
||||
array([[ True, False, True],
|
||||
[ True, True, False]])
|
||||
|
||||
For indexes, an ndarray of booleans is returned.
|
||||
|
||||
>>> index = pd.DatetimeIndex(["2017-07-05", "2017-07-06", None,
|
||||
... "2017-07-08"])
|
||||
>>> index
|
||||
DatetimeIndex(['2017-07-05', '2017-07-06', 'NaT', '2017-07-08'],
|
||||
dtype='datetime64[ns]', freq=None)
|
||||
>>> pd.notna(index)
|
||||
array([ True, True, False, True])
|
||||
|
||||
For Series and DataFrame, the same type is returned, containing booleans.
|
||||
|
||||
>>> df = pd.DataFrame([['ant', 'bee', 'cat'], ['dog', None, 'fly']])
|
||||
>>> df
|
||||
0 1 2
|
||||
0 ant bee cat
|
||||
1 dog None fly
|
||||
>>> pd.notna(df)
|
||||
0 1 2
|
||||
0 True True True
|
||||
1 True False True
|
||||
|
||||
>>> pd.notna(df[1])
|
||||
0 True
|
||||
1 False
|
||||
Name: 1, dtype: bool
|
||||
"""
|
||||
res = isna(obj)
|
||||
if is_scalar(res):
|
||||
return not res
|
||||
return ~res
|
||||
|
||||
|
||||
notnull = notna
|
||||
|
||||
|
||||
def is_null_datelike_scalar(other):
|
||||
""" test whether the object is a null datelike, e.g. Nat
|
||||
but guard against passing a non-scalar """
|
||||
if other is NaT or other is None:
|
||||
return True
|
||||
elif is_scalar(other):
|
||||
|
||||
# a timedelta
|
||||
if hasattr(other, 'dtype'):
|
||||
return other.view('i8') == iNaT
|
||||
elif is_integer(other) and other == iNaT:
|
||||
return True
|
||||
return isna(other)
|
||||
return False
|
||||
|
||||
|
||||
def _isna_compat(arr, fill_value=np.nan):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
arr: a numpy array
|
||||
fill_value: fill value, default to np.nan
|
||||
|
||||
Returns
|
||||
-------
|
||||
True if we can fill using this fill_value
|
||||
"""
|
||||
dtype = arr.dtype
|
||||
if isna(fill_value):
|
||||
return not (is_bool_dtype(dtype) or
|
||||
is_integer_dtype(dtype))
|
||||
return True
|
||||
|
||||
|
||||
def array_equivalent(left, right, strict_nan=False):
|
||||
"""
|
||||
True if two arrays, left and right, have equal non-NaN elements, and NaNs
|
||||
in corresponding locations. False otherwise. It is assumed that left and
|
||||
right are NumPy arrays of the same dtype. The behavior of this function
|
||||
(particularly with respect to NaNs) is not defined if the dtypes are
|
||||
different.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
left, right : ndarrays
|
||||
strict_nan : bool, default False
|
||||
If True, consider NaN and None to be different.
|
||||
|
||||
Returns
|
||||
-------
|
||||
b : bool
|
||||
Returns True if the arrays are equivalent.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> array_equivalent(
|
||||
... np.array([1, 2, np.nan]),
|
||||
... np.array([1, 2, np.nan]))
|
||||
True
|
||||
>>> array_equivalent(
|
||||
... np.array([1, np.nan, 2]),
|
||||
... np.array([1, 2, np.nan]))
|
||||
False
|
||||
"""
|
||||
|
||||
left, right = np.asarray(left), np.asarray(right)
|
||||
|
||||
# shape compat
|
||||
if left.shape != right.shape:
|
||||
return False
|
||||
|
||||
# Object arrays can contain None, NaN and NaT.
|
||||
# string dtypes must be come to this path for NumPy 1.7.1 compat
|
||||
if is_string_dtype(left) or is_string_dtype(right):
|
||||
|
||||
if not strict_nan:
|
||||
# isna considers NaN and None to be equivalent.
|
||||
return lib.array_equivalent_object(
|
||||
_ensure_object(left.ravel()), _ensure_object(right.ravel()))
|
||||
|
||||
for left_value, right_value in zip(left, right):
|
||||
if left_value is NaT and right_value is not NaT:
|
||||
return False
|
||||
|
||||
elif isinstance(left_value, float) and np.isnan(left_value):
|
||||
if (not isinstance(right_value, float) or
|
||||
not np.isnan(right_value)):
|
||||
return False
|
||||
else:
|
||||
if left_value != right_value:
|
||||
return False
|
||||
return True
|
||||
|
||||
# NaNs can occur in float and complex arrays.
|
||||
if is_float_dtype(left) or is_complex_dtype(left):
|
||||
|
||||
# empty
|
||||
if not (np.prod(left.shape) and np.prod(right.shape)):
|
||||
return True
|
||||
return ((left == right) | (isna(left) & isna(right))).all()
|
||||
|
||||
# numpy will will not allow this type of datetimelike vs integer comparison
|
||||
elif is_datetimelike_v_numeric(left, right):
|
||||
return False
|
||||
|
||||
# M8/m8
|
||||
elif needs_i8_conversion(left) and needs_i8_conversion(right):
|
||||
if not is_dtype_equal(left.dtype, right.dtype):
|
||||
return False
|
||||
|
||||
left = left.view('i8')
|
||||
right = right.view('i8')
|
||||
|
||||
# if we have structured dtypes, compare first
|
||||
if (left.dtype.type is np.void or
|
||||
right.dtype.type is np.void):
|
||||
if left.dtype != right.dtype:
|
||||
return False
|
||||
|
||||
return np.array_equal(left, right)
|
||||
|
||||
|
||||
def _infer_fill_value(val):
|
||||
"""
|
||||
infer the fill value for the nan/NaT from the provided
|
||||
scalar/ndarray/list-like if we are a NaT, return the correct dtyped
|
||||
element to provide proper block construction
|
||||
"""
|
||||
|
||||
if not is_list_like(val):
|
||||
val = [val]
|
||||
val = np.array(val, copy=False)
|
||||
if is_datetimelike(val):
|
||||
return np.array('NaT', dtype=val.dtype)
|
||||
elif is_object_dtype(val.dtype):
|
||||
dtype = lib.infer_dtype(_ensure_object(val))
|
||||
if dtype in ['datetime', 'datetime64']:
|
||||
return np.array('NaT', dtype=_NS_DTYPE)
|
||||
elif dtype in ['timedelta', 'timedelta64']:
|
||||
return np.array('NaT', dtype=_TD_DTYPE)
|
||||
return np.nan
|
||||
|
||||
|
||||
def _maybe_fill(arr, fill_value=np.nan):
|
||||
"""
|
||||
if we have a compatible fill_value and arr dtype, then fill
|
||||
"""
|
||||
if _isna_compat(arr, fill_value):
|
||||
arr.fill(fill_value)
|
||||
return arr
|
||||
|
||||
|
||||
def na_value_for_dtype(dtype, compat=True):
|
||||
"""
|
||||
Return a dtype compat na value
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dtype : string / dtype
|
||||
compat : boolean, default True
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.dtype or a pandas dtype
|
||||
"""
|
||||
dtype = pandas_dtype(dtype)
|
||||
|
||||
if is_extension_array_dtype(dtype):
|
||||
return dtype.na_value
|
||||
if (is_datetime64_dtype(dtype) or is_datetime64tz_dtype(dtype) or
|
||||
is_timedelta64_dtype(dtype) or is_period_dtype(dtype)):
|
||||
return NaT
|
||||
elif is_float_dtype(dtype):
|
||||
return np.nan
|
||||
elif is_integer_dtype(dtype):
|
||||
if compat:
|
||||
return 0
|
||||
return np.nan
|
||||
elif is_bool_dtype(dtype):
|
||||
return False
|
||||
return np.nan
|
||||
|
||||
|
||||
def remove_na_arraylike(arr):
|
||||
"""
|
||||
Return array-like containing only true/non-NaN values, possibly empty.
|
||||
"""
|
||||
if is_extension_array_dtype(arr):
|
||||
return arr[notna(arr)]
|
||||
else:
|
||||
return arr[notna(lib.values_from_object(arr))]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
# flake8: noqa
|
||||
from pandas.core.groupby.groupby import (
|
||||
Grouper, GroupBy, SeriesGroupBy, DataFrameGroupBy
|
||||
)
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
# flake8: noqa
|
||||
from pandas.core.indexes.api import *
|
||||
from pandas.core.indexes.multi import _sparsify
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,326 @@
|
||||
"""
|
||||
datetimelike delegation
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from pandas.core.dtypes.generic import ABCSeries
|
||||
from pandas.core.dtypes.common import (
|
||||
is_period_arraylike,
|
||||
is_datetime_arraylike, is_integer_dtype,
|
||||
is_datetime64_dtype, is_datetime64tz_dtype,
|
||||
is_timedelta64_dtype, is_categorical_dtype,
|
||||
is_list_like)
|
||||
|
||||
from pandas.core.accessor import PandasDelegate
|
||||
from pandas.core.base import NoNewAttributesMixin, PandasObject
|
||||
from pandas.core.indexes.datetimes import DatetimeIndex
|
||||
from pandas._libs.tslibs.period import IncompatibleFrequency # noqa
|
||||
from pandas.core.indexes.period import PeriodIndex
|
||||
from pandas.core.indexes.timedeltas import TimedeltaIndex
|
||||
from pandas.core.algorithms import take_1d
|
||||
|
||||
|
||||
class Properties(PandasDelegate, PandasObject, NoNewAttributesMixin):
|
||||
|
||||
def __init__(self, data, orig):
|
||||
if not isinstance(data, ABCSeries):
|
||||
raise TypeError("cannot convert an object of type {0} to a "
|
||||
"datetimelike index".format(type(data)))
|
||||
|
||||
self.values = data
|
||||
self.orig = orig
|
||||
self.name = getattr(data, 'name', None)
|
||||
self.index = getattr(data, 'index', None)
|
||||
self._freeze()
|
||||
|
||||
def _get_values(self):
|
||||
data = self.values
|
||||
if is_datetime64_dtype(data.dtype):
|
||||
return DatetimeIndex(data, copy=False, name=self.name)
|
||||
|
||||
elif is_datetime64tz_dtype(data.dtype):
|
||||
return DatetimeIndex(data, copy=False, name=self.name)
|
||||
|
||||
elif is_timedelta64_dtype(data.dtype):
|
||||
return TimedeltaIndex(data, copy=False, name=self.name)
|
||||
|
||||
else:
|
||||
if is_period_arraylike(data):
|
||||
return PeriodIndex(data, copy=False, name=self.name)
|
||||
if is_datetime_arraylike(data):
|
||||
return DatetimeIndex(data, copy=False, name=self.name)
|
||||
|
||||
raise TypeError("cannot convert an object of type {0} to a "
|
||||
"datetimelike index".format(type(data)))
|
||||
|
||||
def _delegate_property_get(self, name):
|
||||
from pandas import Series
|
||||
values = self._get_values()
|
||||
|
||||
result = getattr(values, name)
|
||||
|
||||
# maybe need to upcast (ints)
|
||||
if isinstance(result, np.ndarray):
|
||||
if is_integer_dtype(result):
|
||||
result = result.astype('int64')
|
||||
elif not is_list_like(result):
|
||||
return result
|
||||
|
||||
result = np.asarray(result)
|
||||
|
||||
# blow up if we operate on categories
|
||||
if self.orig is not None:
|
||||
result = take_1d(result, self.orig.cat.codes)
|
||||
index = self.orig.index
|
||||
else:
|
||||
index = self.index
|
||||
|
||||
# return the result as a Series, which is by definition a copy
|
||||
result = Series(result, index=index, name=self.name)
|
||||
|
||||
# setting this object will show a SettingWithCopyWarning/Error
|
||||
result._is_copy = ("modifications to a property of a datetimelike "
|
||||
"object are not supported and are discarded. "
|
||||
"Change values on the original.")
|
||||
|
||||
return result
|
||||
|
||||
def _delegate_property_set(self, name, value, *args, **kwargs):
|
||||
raise ValueError("modifications to a property of a datetimelike "
|
||||
"object are not supported. Change values on the "
|
||||
"original.")
|
||||
|
||||
def _delegate_method(self, name, *args, **kwargs):
|
||||
from pandas import Series
|
||||
values = self._get_values()
|
||||
|
||||
method = getattr(values, name)
|
||||
result = method(*args, **kwargs)
|
||||
|
||||
if not is_list_like(result):
|
||||
return result
|
||||
|
||||
result = Series(result, index=self.index, name=self.name)
|
||||
|
||||
# setting this object will show a SettingWithCopyWarning/Error
|
||||
result._is_copy = ("modifications to a method of a datetimelike "
|
||||
"object are not supported and are discarded. "
|
||||
"Change values on the original.")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class DatetimeProperties(Properties):
|
||||
"""
|
||||
Accessor object for datetimelike properties of the Series values.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> s.dt.hour
|
||||
>>> s.dt.second
|
||||
>>> s.dt.quarter
|
||||
|
||||
Returns a Series indexed like the original Series.
|
||||
Raises TypeError if the Series does not contain datetimelike values.
|
||||
"""
|
||||
|
||||
def to_pydatetime(self):
|
||||
"""
|
||||
Return the data as an array of native Python datetime objects
|
||||
|
||||
Timezone information is retained if present.
|
||||
|
||||
.. warning::
|
||||
|
||||
Python's datetime uses microsecond resolution, which is lower than
|
||||
pandas (nanosecond). The values are truncated.
|
||||
|
||||
Returns
|
||||
-------
|
||||
numpy.ndarray
|
||||
object dtype array containing native Python datetime objects.
|
||||
|
||||
See Also
|
||||
--------
|
||||
datetime.datetime : Standard library value for a datetime.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> s = pd.Series(pd.date_range('20180310', periods=2))
|
||||
>>> s
|
||||
0 2018-03-10
|
||||
1 2018-03-11
|
||||
dtype: datetime64[ns]
|
||||
|
||||
>>> s.dt.to_pydatetime()
|
||||
array([datetime.datetime(2018, 3, 10, 0, 0),
|
||||
datetime.datetime(2018, 3, 11, 0, 0)], dtype=object)
|
||||
|
||||
pandas' nanosecond precision is truncated to microseconds.
|
||||
|
||||
>>> s = pd.Series(pd.date_range('20180310', periods=2, freq='ns'))
|
||||
>>> s
|
||||
0 2018-03-10 00:00:00.000000000
|
||||
1 2018-03-10 00:00:00.000000001
|
||||
dtype: datetime64[ns]
|
||||
|
||||
>>> s.dt.to_pydatetime()
|
||||
array([datetime.datetime(2018, 3, 10, 0, 0),
|
||||
datetime.datetime(2018, 3, 10, 0, 0)], dtype=object)
|
||||
"""
|
||||
return self._get_values().to_pydatetime()
|
||||
|
||||
@property
|
||||
def freq(self):
|
||||
return self._get_values().inferred_freq
|
||||
|
||||
|
||||
DatetimeProperties._add_delegate_accessors(
|
||||
delegate=DatetimeIndex,
|
||||
accessors=DatetimeIndex._datetimelike_ops,
|
||||
typ='property')
|
||||
DatetimeProperties._add_delegate_accessors(
|
||||
delegate=DatetimeIndex,
|
||||
accessors=DatetimeIndex._datetimelike_methods,
|
||||
typ='method')
|
||||
|
||||
|
||||
class TimedeltaProperties(Properties):
|
||||
"""
|
||||
Accessor object for datetimelike properties of the Series values.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> s.dt.hours
|
||||
>>> s.dt.seconds
|
||||
|
||||
Returns a Series indexed like the original Series.
|
||||
Raises TypeError if the Series does not contain datetimelike values.
|
||||
"""
|
||||
|
||||
def to_pytimedelta(self):
|
||||
"""
|
||||
Return an array of native `datetime.timedelta` objects.
|
||||
|
||||
Python's standard `datetime` library uses a different representation
|
||||
timedelta's. This method converts a Series of pandas Timedeltas
|
||||
to `datetime.timedelta` format with the same length as the original
|
||||
Series.
|
||||
|
||||
Returns
|
||||
-------
|
||||
a : numpy.ndarray
|
||||
1D array containing data with `datetime.timedelta` type.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> s = pd.Series(pd.to_timedelta(np.arange(5), unit='d'))
|
||||
>>> s
|
||||
0 0 days
|
||||
1 1 days
|
||||
2 2 days
|
||||
3 3 days
|
||||
4 4 days
|
||||
dtype: timedelta64[ns]
|
||||
|
||||
>>> s.dt.to_pytimedelta()
|
||||
array([datetime.timedelta(0), datetime.timedelta(1),
|
||||
datetime.timedelta(2), datetime.timedelta(3),
|
||||
datetime.timedelta(4)], dtype=object)
|
||||
|
||||
See Also
|
||||
--------
|
||||
datetime.timedelta
|
||||
"""
|
||||
return self._get_values().to_pytimedelta()
|
||||
|
||||
@property
|
||||
def components(self):
|
||||
"""
|
||||
Return a dataframe of the components (days, hours, minutes,
|
||||
seconds, milliseconds, microseconds, nanoseconds) of the Timedeltas.
|
||||
|
||||
Returns
|
||||
-------
|
||||
a DataFrame
|
||||
|
||||
"""
|
||||
return self._get_values().components.set_index(self.index)
|
||||
|
||||
@property
|
||||
def freq(self):
|
||||
return self._get_values().inferred_freq
|
||||
|
||||
|
||||
TimedeltaProperties._add_delegate_accessors(
|
||||
delegate=TimedeltaIndex,
|
||||
accessors=TimedeltaIndex._datetimelike_ops,
|
||||
typ='property')
|
||||
TimedeltaProperties._add_delegate_accessors(
|
||||
delegate=TimedeltaIndex,
|
||||
accessors=TimedeltaIndex._datetimelike_methods,
|
||||
typ='method')
|
||||
|
||||
|
||||
class PeriodProperties(Properties):
|
||||
"""
|
||||
Accessor object for datetimelike properties of the Series values.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> s.dt.hour
|
||||
>>> s.dt.second
|
||||
>>> s.dt.quarter
|
||||
|
||||
Returns a Series indexed like the original Series.
|
||||
Raises TypeError if the Series does not contain datetimelike values.
|
||||
"""
|
||||
|
||||
|
||||
PeriodProperties._add_delegate_accessors(
|
||||
delegate=PeriodIndex,
|
||||
accessors=PeriodIndex._datetimelike_ops,
|
||||
typ='property')
|
||||
PeriodProperties._add_delegate_accessors(
|
||||
delegate=PeriodIndex,
|
||||
accessors=PeriodIndex._datetimelike_methods,
|
||||
typ='method')
|
||||
|
||||
|
||||
class CombinedDatetimelikeProperties(DatetimeProperties, TimedeltaProperties):
|
||||
|
||||
def __new__(cls, data):
|
||||
# CombinedDatetimelikeProperties isn't really instantiated. Instead
|
||||
# we need to choose which parent (datetime or timedelta) is
|
||||
# appropriate. Since we're checking the dtypes anyway, we'll just
|
||||
# do all the validation here.
|
||||
from pandas import Series
|
||||
|
||||
if not isinstance(data, Series):
|
||||
raise TypeError("cannot convert an object of type {0} to a "
|
||||
"datetimelike index".format(type(data)))
|
||||
|
||||
orig = data if is_categorical_dtype(data) else None
|
||||
if orig is not None:
|
||||
data = Series(orig.values.categories,
|
||||
name=orig.name,
|
||||
copy=False)
|
||||
|
||||
try:
|
||||
if is_datetime64_dtype(data.dtype):
|
||||
return DatetimeProperties(data, orig)
|
||||
elif is_datetime64tz_dtype(data.dtype):
|
||||
return DatetimeProperties(data, orig)
|
||||
elif is_timedelta64_dtype(data.dtype):
|
||||
return TimedeltaProperties(data, orig)
|
||||
else:
|
||||
if is_period_arraylike(data):
|
||||
return PeriodProperties(data, orig)
|
||||
if is_datetime_arraylike(data):
|
||||
return DatetimeProperties(data, orig)
|
||||
except Exception:
|
||||
pass # we raise an attribute error anyway
|
||||
|
||||
raise AttributeError("Can only use .dt accessor with datetimelike "
|
||||
"values")
|
||||
@@ -0,0 +1,163 @@
|
||||
import textwrap
|
||||
import warnings
|
||||
|
||||
from pandas.core.indexes.base import (Index,
|
||||
_new_Index,
|
||||
_ensure_index,
|
||||
_ensure_index_from_sequences,
|
||||
InvalidIndexError) # noqa
|
||||
from pandas.core.indexes.category import CategoricalIndex # noqa
|
||||
from pandas.core.indexes.multi import MultiIndex # noqa
|
||||
from pandas.core.indexes.interval import IntervalIndex # noqa
|
||||
from pandas.core.indexes.numeric import (NumericIndex, Float64Index, # noqa
|
||||
Int64Index, UInt64Index)
|
||||
from pandas.core.indexes.range import RangeIndex # noqa
|
||||
from pandas.core.indexes.timedeltas import TimedeltaIndex
|
||||
from pandas.core.indexes.period import PeriodIndex
|
||||
from pandas.core.indexes.datetimes import DatetimeIndex
|
||||
|
||||
import pandas.core.common as com
|
||||
from pandas._libs import lib
|
||||
from pandas._libs.tslib import NaT
|
||||
|
||||
_sort_msg = textwrap.dedent("""\
|
||||
Sorting because non-concatenation axis is not aligned. A future version
|
||||
of pandas will change to not sort by default.
|
||||
|
||||
To accept the future behavior, pass 'sort=False'.
|
||||
|
||||
To retain the current behavior and silence the warning, pass 'sort=True'.
|
||||
""")
|
||||
|
||||
|
||||
# TODO: there are many places that rely on these private methods existing in
|
||||
# pandas.core.index
|
||||
__all__ = ['Index', 'MultiIndex', 'NumericIndex', 'Float64Index', 'Int64Index',
|
||||
'CategoricalIndex', 'IntervalIndex', 'RangeIndex', 'UInt64Index',
|
||||
'InvalidIndexError', 'TimedeltaIndex',
|
||||
'PeriodIndex', 'DatetimeIndex',
|
||||
'_new_Index', 'NaT',
|
||||
'_ensure_index', '_ensure_index_from_sequences',
|
||||
'_get_combined_index',
|
||||
'_get_objs_combined_axis', '_union_indexes',
|
||||
'_get_consensus_names',
|
||||
'_all_indexes_same']
|
||||
|
||||
|
||||
def _get_objs_combined_axis(objs, intersect=False, axis=0, sort=True):
|
||||
# Extract combined index: return intersection or union (depending on the
|
||||
# value of "intersect") of indexes on given axis, or None if all objects
|
||||
# lack indexes (e.g. they are numpy arrays)
|
||||
obs_idxes = [obj._get_axis(axis) for obj in objs
|
||||
if hasattr(obj, '_get_axis')]
|
||||
if obs_idxes:
|
||||
return _get_combined_index(obs_idxes, intersect=intersect, sort=sort)
|
||||
|
||||
|
||||
def _get_combined_index(indexes, intersect=False, sort=False):
|
||||
# TODO: handle index names!
|
||||
indexes = com._get_distinct_objs(indexes)
|
||||
if len(indexes) == 0:
|
||||
index = Index([])
|
||||
elif len(indexes) == 1:
|
||||
index = indexes[0]
|
||||
elif intersect:
|
||||
index = indexes[0]
|
||||
for other in indexes[1:]:
|
||||
index = index.intersection(other)
|
||||
else:
|
||||
index = _union_indexes(indexes, sort=sort)
|
||||
index = _ensure_index(index)
|
||||
|
||||
if sort:
|
||||
try:
|
||||
index = index.sort_values()
|
||||
except TypeError:
|
||||
pass
|
||||
return index
|
||||
|
||||
|
||||
def _union_indexes(indexes, sort=True):
|
||||
if len(indexes) == 0:
|
||||
raise AssertionError('Must have at least 1 Index to union')
|
||||
if len(indexes) == 1:
|
||||
result = indexes[0]
|
||||
if isinstance(result, list):
|
||||
result = Index(sorted(result))
|
||||
return result
|
||||
|
||||
indexes, kind = _sanitize_and_check(indexes)
|
||||
|
||||
def _unique_indices(inds):
|
||||
def conv(i):
|
||||
if isinstance(i, Index):
|
||||
i = i.tolist()
|
||||
return i
|
||||
|
||||
return Index(
|
||||
lib.fast_unique_multiple_list([conv(i) for i in inds], sort=sort))
|
||||
|
||||
if kind == 'special':
|
||||
result = indexes[0]
|
||||
|
||||
if hasattr(result, 'union_many'):
|
||||
return result.union_many(indexes[1:])
|
||||
else:
|
||||
for other in indexes[1:]:
|
||||
result = result.union(other)
|
||||
return result
|
||||
elif kind == 'array':
|
||||
index = indexes[0]
|
||||
for other in indexes[1:]:
|
||||
if not index.equals(other):
|
||||
|
||||
if sort is None:
|
||||
# TODO: remove once pd.concat sort default changes
|
||||
warnings.warn(_sort_msg, FutureWarning, stacklevel=8)
|
||||
sort = True
|
||||
|
||||
return _unique_indices(indexes)
|
||||
|
||||
name = _get_consensus_names(indexes)[0]
|
||||
if name != index.name:
|
||||
index = index._shallow_copy(name=name)
|
||||
return index
|
||||
else: # kind='list'
|
||||
return _unique_indices(indexes)
|
||||
|
||||
|
||||
def _sanitize_and_check(indexes):
|
||||
kinds = list({type(index) for index in indexes})
|
||||
|
||||
if list in kinds:
|
||||
if len(kinds) > 1:
|
||||
indexes = [Index(com._try_sort(x))
|
||||
if not isinstance(x, Index) else
|
||||
x for x in indexes]
|
||||
kinds.remove(list)
|
||||
else:
|
||||
return indexes, 'list'
|
||||
|
||||
if len(kinds) > 1 or Index not in kinds:
|
||||
return indexes, 'special'
|
||||
else:
|
||||
return indexes, 'array'
|
||||
|
||||
|
||||
def _get_consensus_names(indexes):
|
||||
|
||||
# find the non-none names, need to tupleify to make
|
||||
# the set hashable, then reverse on return
|
||||
consensus_names = set(tuple(i.names) for i in indexes
|
||||
if com._any_not_none(*i.names))
|
||||
if len(consensus_names) == 1:
|
||||
return list(list(consensus_names)[0])
|
||||
return [None] * indexes[0].nlevels
|
||||
|
||||
|
||||
def _all_indexes_same(indexes):
|
||||
first = indexes[0]
|
||||
for index in indexes[1:]:
|
||||
if not first.equals(index):
|
||||
return False
|
||||
return True
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,870 @@
|
||||
import operator
|
||||
|
||||
import numpy as np
|
||||
from pandas._libs import index as libindex
|
||||
|
||||
from pandas import compat
|
||||
from pandas.compat.numpy import function as nv
|
||||
from pandas.core.dtypes.generic import ABCCategorical, ABCSeries
|
||||
from pandas.core.dtypes.dtypes import CategoricalDtype
|
||||
from pandas.core.dtypes.common import (
|
||||
is_categorical_dtype,
|
||||
_ensure_platform_int,
|
||||
is_list_like,
|
||||
is_interval_dtype,
|
||||
is_scalar)
|
||||
from pandas.core.dtypes.missing import array_equivalent, isna
|
||||
from pandas.core.algorithms import take_1d
|
||||
|
||||
|
||||
from pandas.util._decorators import Appender, cache_readonly
|
||||
from pandas.core.config import get_option
|
||||
from pandas.core.indexes.base import Index, _index_shared_docs
|
||||
from pandas.core import accessor
|
||||
import pandas.core.common as com
|
||||
import pandas.core.missing as missing
|
||||
import pandas.core.indexes.base as ibase
|
||||
|
||||
_index_doc_kwargs = dict(ibase._index_doc_kwargs)
|
||||
_index_doc_kwargs.update(dict(target_klass='CategoricalIndex'))
|
||||
|
||||
|
||||
class CategoricalIndex(Index, accessor.PandasDelegate):
|
||||
"""
|
||||
|
||||
Immutable Index implementing an ordered, sliceable set. CategoricalIndex
|
||||
represents a sparsely populated Index with an underlying Categorical.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : array-like or Categorical, (1-dimensional)
|
||||
categories : optional, array-like
|
||||
categories for the CategoricalIndex
|
||||
ordered : boolean,
|
||||
designating if the categories are ordered
|
||||
copy : bool
|
||||
Make a copy of input ndarray
|
||||
name : object
|
||||
Name to be stored in the index
|
||||
|
||||
Attributes
|
||||
----------
|
||||
codes
|
||||
categories
|
||||
ordered
|
||||
|
||||
Methods
|
||||
-------
|
||||
rename_categories
|
||||
reorder_categories
|
||||
add_categories
|
||||
remove_categories
|
||||
remove_unused_categories
|
||||
set_categories
|
||||
as_ordered
|
||||
as_unordered
|
||||
map
|
||||
|
||||
See Also
|
||||
--------
|
||||
Categorical, Index
|
||||
"""
|
||||
|
||||
_typ = 'categoricalindex'
|
||||
_engine_type = libindex.Int64Engine
|
||||
_attributes = ['name']
|
||||
|
||||
def __new__(cls, data=None, categories=None, ordered=None, dtype=None,
|
||||
copy=False, name=None, fastpath=False):
|
||||
|
||||
if fastpath:
|
||||
return cls._simple_new(data, name=name, dtype=dtype)
|
||||
|
||||
if name is None and hasattr(data, 'name'):
|
||||
name = data.name
|
||||
|
||||
if isinstance(data, ABCCategorical):
|
||||
data = cls._create_categorical(cls, data, categories, ordered,
|
||||
dtype)
|
||||
elif isinstance(data, CategoricalIndex):
|
||||
data = data._data
|
||||
data = cls._create_categorical(cls, data, categories, ordered,
|
||||
dtype)
|
||||
else:
|
||||
|
||||
# don't allow scalars
|
||||
# if data is None, then categories must be provided
|
||||
if is_scalar(data):
|
||||
if data is not None or categories is None:
|
||||
cls._scalar_data_error(data)
|
||||
data = []
|
||||
data = cls._create_categorical(cls, data, categories, ordered,
|
||||
dtype)
|
||||
|
||||
if copy:
|
||||
data = data.copy()
|
||||
|
||||
return cls._simple_new(data, name=name)
|
||||
|
||||
def _create_from_codes(self, codes, categories=None, ordered=None,
|
||||
name=None):
|
||||
"""
|
||||
*this is an internal non-public method*
|
||||
|
||||
create the correct categorical from codes
|
||||
|
||||
Parameters
|
||||
----------
|
||||
codes : new codes
|
||||
categories : optional categories, defaults to existing
|
||||
ordered : optional ordered attribute, defaults to existing
|
||||
name : optional name attribute, defaults to existing
|
||||
|
||||
Returns
|
||||
-------
|
||||
CategoricalIndex
|
||||
"""
|
||||
|
||||
from pandas.core.arrays import Categorical
|
||||
if categories is None:
|
||||
categories = self.categories
|
||||
if ordered is None:
|
||||
ordered = self.ordered
|
||||
if name is None:
|
||||
name = self.name
|
||||
cat = Categorical.from_codes(codes, categories=categories,
|
||||
ordered=self.ordered)
|
||||
return CategoricalIndex(cat, name=name)
|
||||
|
||||
@staticmethod
|
||||
def _create_categorical(self, data, categories=None, ordered=None,
|
||||
dtype=None):
|
||||
"""
|
||||
*this is an internal non-public method*
|
||||
|
||||
create the correct categorical from data and the properties
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : data for new Categorical
|
||||
categories : optional categories, defaults to existing
|
||||
ordered : optional ordered attribute, defaults to existing
|
||||
dtype : CategoricalDtype, defaults to existing
|
||||
|
||||
Returns
|
||||
-------
|
||||
Categorical
|
||||
"""
|
||||
if (isinstance(data, (ABCSeries, type(self))) and
|
||||
is_categorical_dtype(data)):
|
||||
data = data.values
|
||||
|
||||
if not isinstance(data, ABCCategorical):
|
||||
if ordered is None and dtype is None:
|
||||
ordered = False
|
||||
from pandas.core.arrays import Categorical
|
||||
data = Categorical(data, categories=categories, ordered=ordered,
|
||||
dtype=dtype)
|
||||
else:
|
||||
if categories is not None:
|
||||
data = data.set_categories(categories, ordered=ordered)
|
||||
elif ordered is not None and ordered != data.ordered:
|
||||
data = data.set_ordered(ordered)
|
||||
if isinstance(dtype, CategoricalDtype):
|
||||
# we want to silently ignore dtype='category'
|
||||
data = data._set_dtype(dtype)
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def _simple_new(cls, values, name=None, categories=None, ordered=None,
|
||||
dtype=None, **kwargs):
|
||||
result = object.__new__(cls)
|
||||
|
||||
values = cls._create_categorical(cls, values, categories, ordered,
|
||||
dtype=dtype)
|
||||
result._data = values
|
||||
result.name = name
|
||||
for k, v in compat.iteritems(kwargs):
|
||||
setattr(result, k, v)
|
||||
|
||||
result._reset_identity()
|
||||
return result
|
||||
|
||||
@Appender(_index_shared_docs['_shallow_copy'])
|
||||
def _shallow_copy(self, values=None, categories=None, ordered=None,
|
||||
dtype=None, **kwargs):
|
||||
# categories and ordered can't be part of attributes,
|
||||
# as these are properties
|
||||
# we want to reuse self.dtype if possible, i.e. neither are
|
||||
# overridden.
|
||||
if dtype is not None and (categories is not None or
|
||||
ordered is not None):
|
||||
raise TypeError("Cannot specify both `dtype` and `categories` "
|
||||
"or `ordered`")
|
||||
|
||||
if categories is None and ordered is None:
|
||||
dtype = self.dtype if dtype is None else dtype
|
||||
return super(CategoricalIndex, self)._shallow_copy(
|
||||
values=values, dtype=dtype, **kwargs)
|
||||
if categories is None:
|
||||
categories = self.categories
|
||||
if ordered is None:
|
||||
ordered = self.ordered
|
||||
|
||||
return super(CategoricalIndex, self)._shallow_copy(
|
||||
values=values, categories=categories,
|
||||
ordered=ordered, **kwargs)
|
||||
|
||||
def _is_dtype_compat(self, other):
|
||||
"""
|
||||
*this is an internal non-public method*
|
||||
|
||||
provide a comparison between the dtype of self and other (coercing if
|
||||
needed)
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError if the dtypes are not compatible
|
||||
"""
|
||||
if is_categorical_dtype(other):
|
||||
if isinstance(other, CategoricalIndex):
|
||||
other = other._values
|
||||
if not other.is_dtype_equal(self):
|
||||
raise TypeError("categories must match existing categories "
|
||||
"when appending")
|
||||
else:
|
||||
values = other
|
||||
if not is_list_like(values):
|
||||
values = [values]
|
||||
other = CategoricalIndex(self._create_categorical(
|
||||
self, other, categories=self.categories, ordered=self.ordered))
|
||||
if not other.isin(values).all():
|
||||
raise TypeError("cannot append a non-category item to a "
|
||||
"CategoricalIndex")
|
||||
|
||||
return other
|
||||
|
||||
def equals(self, other):
|
||||
"""
|
||||
Determines if two CategorialIndex objects contain the same elements.
|
||||
"""
|
||||
if self.is_(other):
|
||||
return True
|
||||
|
||||
if not isinstance(other, Index):
|
||||
return False
|
||||
|
||||
try:
|
||||
other = self._is_dtype_compat(other)
|
||||
return array_equivalent(self._data, other)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def _formatter_func(self):
|
||||
return self.categories._formatter_func
|
||||
|
||||
def _format_attrs(self):
|
||||
"""
|
||||
Return a list of tuples of the (attr,formatted_value)
|
||||
"""
|
||||
max_categories = (10 if get_option("display.max_categories") == 0 else
|
||||
get_option("display.max_categories"))
|
||||
attrs = [
|
||||
('categories',
|
||||
ibase.default_pprint(self.categories,
|
||||
max_seq_items=max_categories)),
|
||||
('ordered', self.ordered)]
|
||||
if self.name is not None:
|
||||
attrs.append(('name', ibase.default_pprint(self.name)))
|
||||
attrs.append(('dtype', "'%s'" % self.dtype.name))
|
||||
max_seq_items = get_option('display.max_seq_items') or len(self)
|
||||
if len(self) > max_seq_items:
|
||||
attrs.append(('length', len(self)))
|
||||
return attrs
|
||||
|
||||
@property
|
||||
def inferred_type(self):
|
||||
return 'categorical'
|
||||
|
||||
@property
|
||||
def values(self):
|
||||
""" return the underlying data, which is a Categorical """
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def itemsize(self):
|
||||
# Size of the items in categories, not codes.
|
||||
return self.values.itemsize
|
||||
|
||||
def get_values(self):
|
||||
""" return the underlying data as an ndarray """
|
||||
return self._data.get_values()
|
||||
|
||||
def tolist(self):
|
||||
return self._data.tolist()
|
||||
|
||||
@property
|
||||
def codes(self):
|
||||
return self._data.codes
|
||||
|
||||
@property
|
||||
def categories(self):
|
||||
return self._data.categories
|
||||
|
||||
@property
|
||||
def ordered(self):
|
||||
return self._data.ordered
|
||||
|
||||
def _reverse_indexer(self):
|
||||
return self._data._reverse_indexer()
|
||||
|
||||
@Appender(_index_shared_docs['__contains__'] % _index_doc_kwargs)
|
||||
def __contains__(self, key):
|
||||
hash(key)
|
||||
|
||||
if self.categories._defer_to_indexing:
|
||||
return key in self.categories
|
||||
|
||||
return key in self.values
|
||||
|
||||
@Appender(_index_shared_docs['contains'] % _index_doc_kwargs)
|
||||
def contains(self, key):
|
||||
hash(key)
|
||||
|
||||
if self.categories._defer_to_indexing:
|
||||
return self.categories.contains(key)
|
||||
|
||||
return key in self.values
|
||||
|
||||
def __array__(self, dtype=None):
|
||||
""" the array interface, return my values """
|
||||
return np.array(self._data, dtype=dtype)
|
||||
|
||||
@Appender(_index_shared_docs['astype'])
|
||||
def astype(self, dtype, copy=True):
|
||||
if is_interval_dtype(dtype):
|
||||
from pandas import IntervalIndex
|
||||
return IntervalIndex(np.array(self))
|
||||
elif is_categorical_dtype(dtype):
|
||||
# GH 18630
|
||||
dtype = self.dtype.update_dtype(dtype)
|
||||
if dtype == self.dtype:
|
||||
return self.copy() if copy else self
|
||||
|
||||
return super(CategoricalIndex, self).astype(dtype=dtype, copy=copy)
|
||||
|
||||
@cache_readonly
|
||||
def _isnan(self):
|
||||
""" return if each value is nan"""
|
||||
return self._data.codes == -1
|
||||
|
||||
@Appender(ibase._index_shared_docs['fillna'])
|
||||
def fillna(self, value, downcast=None):
|
||||
self._assert_can_do_op(value)
|
||||
return CategoricalIndex(self._data.fillna(value), name=self.name)
|
||||
|
||||
def argsort(self, *args, **kwargs):
|
||||
return self.values.argsort(*args, **kwargs)
|
||||
|
||||
@cache_readonly
|
||||
def _engine(self):
|
||||
|
||||
# we are going to look things up with the codes themselves
|
||||
return self._engine_type(lambda: self.codes.astype('i8'), len(self))
|
||||
|
||||
# introspection
|
||||
@cache_readonly
|
||||
def is_unique(self):
|
||||
return self._engine.is_unique
|
||||
|
||||
@property
|
||||
def is_monotonic_increasing(self):
|
||||
return self._engine.is_monotonic_increasing
|
||||
|
||||
@property
|
||||
def is_monotonic_decreasing(self):
|
||||
return self._engine.is_monotonic_decreasing
|
||||
|
||||
@Appender(_index_shared_docs['index_unique'] % _index_doc_kwargs)
|
||||
def unique(self, level=None):
|
||||
if level is not None:
|
||||
self._validate_index_level(level)
|
||||
result = self.values.unique()
|
||||
# CategoricalIndex._shallow_copy keeps original categories
|
||||
# and ordered if not otherwise specified
|
||||
return self._shallow_copy(result, categories=result.categories,
|
||||
ordered=result.ordered)
|
||||
|
||||
@Appender(Index.duplicated.__doc__)
|
||||
def duplicated(self, keep='first'):
|
||||
from pandas._libs.hashtable import duplicated_int64
|
||||
codes = self.codes.astype('i8')
|
||||
return duplicated_int64(codes, keep)
|
||||
|
||||
def _to_safe_for_reshape(self):
|
||||
""" convert to object if we are a categorical """
|
||||
return self.astype('object')
|
||||
|
||||
def get_loc(self, key, method=None):
|
||||
"""
|
||||
Get integer location, slice or boolean mask for requested label.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
key : label
|
||||
method : {None}
|
||||
* default: exact matches only.
|
||||
|
||||
Returns
|
||||
-------
|
||||
loc : int if unique index, slice if monotonic index, else mask
|
||||
|
||||
Examples
|
||||
---------
|
||||
>>> unique_index = pd.CategoricalIndex(list('abc'))
|
||||
>>> unique_index.get_loc('b')
|
||||
1
|
||||
|
||||
>>> monotonic_index = pd.CategoricalIndex(list('abbc'))
|
||||
>>> monotonic_index.get_loc('b')
|
||||
slice(1, 3, None)
|
||||
|
||||
>>> non_monotonic_index = p.dCategoricalIndex(list('abcb'))
|
||||
>>> non_monotonic_index.get_loc('b')
|
||||
array([False, True, False, True], dtype=bool)
|
||||
"""
|
||||
codes = self.categories.get_loc(key)
|
||||
if (codes == -1):
|
||||
raise KeyError(key)
|
||||
return self._engine.get_loc(codes)
|
||||
|
||||
def get_value(self, series, key):
|
||||
"""
|
||||
Fast lookup of value from 1-dimensional ndarray. Only use this if you
|
||||
know what you're doing
|
||||
"""
|
||||
try:
|
||||
k = com._values_from_object(key)
|
||||
k = self._convert_scalar_indexer(k, kind='getitem')
|
||||
indexer = self.get_loc(k)
|
||||
return series.iloc[indexer]
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
|
||||
# we might be a positional inexer
|
||||
return super(CategoricalIndex, self).get_value(series, key)
|
||||
|
||||
def _can_reindex(self, indexer):
|
||||
""" always allow reindexing """
|
||||
pass
|
||||
|
||||
@Appender(_index_shared_docs['where'])
|
||||
def where(self, cond, other=None):
|
||||
if other is None:
|
||||
other = self._na_value
|
||||
values = np.where(cond, self.values, other)
|
||||
|
||||
from pandas.core.arrays import Categorical
|
||||
cat = Categorical(values,
|
||||
categories=self.categories,
|
||||
ordered=self.ordered)
|
||||
return self._shallow_copy(cat, **self._get_attributes_dict())
|
||||
|
||||
def reindex(self, target, method=None, level=None, limit=None,
|
||||
tolerance=None):
|
||||
"""
|
||||
Create index with target's values (move/add/delete values as necessary)
|
||||
|
||||
Returns
|
||||
-------
|
||||
new_index : pd.Index
|
||||
Resulting index
|
||||
indexer : np.ndarray or None
|
||||
Indices of output values in original index
|
||||
|
||||
"""
|
||||
|
||||
if method is not None:
|
||||
raise NotImplementedError("argument method is not implemented for "
|
||||
"CategoricalIndex.reindex")
|
||||
if level is not None:
|
||||
raise NotImplementedError("argument level is not implemented for "
|
||||
"CategoricalIndex.reindex")
|
||||
if limit is not None:
|
||||
raise NotImplementedError("argument limit is not implemented for "
|
||||
"CategoricalIndex.reindex")
|
||||
|
||||
target = ibase._ensure_index(target)
|
||||
|
||||
if not is_categorical_dtype(target) and not target.is_unique:
|
||||
raise ValueError("cannot reindex with a non-unique indexer")
|
||||
|
||||
indexer, missing = self.get_indexer_non_unique(np.array(target))
|
||||
|
||||
if len(self.codes):
|
||||
new_target = self.take(indexer)
|
||||
else:
|
||||
new_target = target
|
||||
|
||||
# filling in missing if needed
|
||||
if len(missing):
|
||||
cats = self.categories.get_indexer(target)
|
||||
|
||||
if (cats == -1).any():
|
||||
# coerce to a regular index here!
|
||||
result = Index(np.array(self), name=self.name)
|
||||
new_target, indexer, _ = result._reindex_non_unique(
|
||||
np.array(target))
|
||||
else:
|
||||
|
||||
codes = new_target.codes.copy()
|
||||
codes[indexer == -1] = cats[missing]
|
||||
new_target = self._create_from_codes(codes)
|
||||
|
||||
# we always want to return an Index type here
|
||||
# to be consistent with .reindex for other index types (e.g. they don't
|
||||
# coerce based on the actual values, only on the dtype)
|
||||
# unless we had an initial Categorical to begin with
|
||||
# in which case we are going to conform to the passed Categorical
|
||||
new_target = np.asarray(new_target)
|
||||
if is_categorical_dtype(target):
|
||||
new_target = target._shallow_copy(new_target, name=self.name)
|
||||
else:
|
||||
new_target = Index(new_target, name=self.name)
|
||||
|
||||
return new_target, indexer
|
||||
|
||||
def _reindex_non_unique(self, target):
|
||||
""" reindex from a non-unique; which CategoricalIndex's are almost
|
||||
always
|
||||
"""
|
||||
new_target, indexer = self.reindex(target)
|
||||
new_indexer = None
|
||||
|
||||
check = indexer == -1
|
||||
if check.any():
|
||||
new_indexer = np.arange(len(self.take(indexer)))
|
||||
new_indexer[check] = -1
|
||||
|
||||
cats = self.categories.get_indexer(target)
|
||||
if not (cats == -1).any():
|
||||
# .reindex returns normal Index. Revert to CategoricalIndex if
|
||||
# all targets are included in my categories
|
||||
new_target = self._shallow_copy(new_target)
|
||||
|
||||
return new_target, indexer, new_indexer
|
||||
|
||||
@Appender(_index_shared_docs['get_indexer'] % _index_doc_kwargs)
|
||||
def get_indexer(self, target, method=None, limit=None, tolerance=None):
|
||||
from pandas.core.arrays.categorical import _recode_for_categories
|
||||
|
||||
method = missing.clean_reindex_fill_method(method)
|
||||
target = ibase._ensure_index(target)
|
||||
|
||||
if self.is_unique and self.equals(target):
|
||||
return np.arange(len(self), dtype='intp')
|
||||
|
||||
if method == 'pad' or method == 'backfill':
|
||||
raise NotImplementedError("method='pad' and method='backfill' not "
|
||||
"implemented yet for CategoricalIndex")
|
||||
elif method == 'nearest':
|
||||
raise NotImplementedError("method='nearest' not implemented yet "
|
||||
'for CategoricalIndex')
|
||||
|
||||
if (isinstance(target, CategoricalIndex) and
|
||||
self.values.is_dtype_equal(target)):
|
||||
if self.values.equals(target.values):
|
||||
# we have the same codes
|
||||
codes = target.codes
|
||||
else:
|
||||
codes = _recode_for_categories(target.codes,
|
||||
target.categories,
|
||||
self.values.categories)
|
||||
else:
|
||||
if isinstance(target, CategoricalIndex):
|
||||
code_indexer = self.categories.get_indexer(target.categories)
|
||||
codes = take_1d(code_indexer, target.codes, fill_value=-1)
|
||||
else:
|
||||
codes = self.categories.get_indexer(target)
|
||||
|
||||
indexer, _ = self._engine.get_indexer_non_unique(codes)
|
||||
return _ensure_platform_int(indexer)
|
||||
|
||||
@Appender(_index_shared_docs['get_indexer_non_unique'] % _index_doc_kwargs)
|
||||
def get_indexer_non_unique(self, target):
|
||||
target = ibase._ensure_index(target)
|
||||
|
||||
if isinstance(target, CategoricalIndex):
|
||||
# Indexing on codes is more efficient if categories are the same:
|
||||
if target.categories is self.categories:
|
||||
target = target.codes
|
||||
indexer, missing = self._engine.get_indexer_non_unique(target)
|
||||
return _ensure_platform_int(indexer), missing
|
||||
target = target.values
|
||||
|
||||
codes = self.categories.get_indexer(target)
|
||||
indexer, missing = self._engine.get_indexer_non_unique(codes)
|
||||
return _ensure_platform_int(indexer), missing
|
||||
|
||||
@Appender(_index_shared_docs['_convert_scalar_indexer'])
|
||||
def _convert_scalar_indexer(self, key, kind=None):
|
||||
if self.categories._defer_to_indexing:
|
||||
return self.categories._convert_scalar_indexer(key, kind=kind)
|
||||
|
||||
return super(CategoricalIndex, self)._convert_scalar_indexer(
|
||||
key, kind=kind)
|
||||
|
||||
@Appender(_index_shared_docs['_convert_list_indexer'])
|
||||
def _convert_list_indexer(self, keyarr, kind=None):
|
||||
# Return our indexer or raise if all of the values are not included in
|
||||
# the categories
|
||||
|
||||
if self.categories._defer_to_indexing:
|
||||
indexer = self.categories._convert_list_indexer(keyarr, kind=kind)
|
||||
return Index(self.codes).get_indexer_for(indexer)
|
||||
|
||||
indexer = self.categories.get_indexer(np.asarray(keyarr))
|
||||
if (indexer == -1).any():
|
||||
raise KeyError(
|
||||
"a list-indexer must only "
|
||||
"include values that are "
|
||||
"in the categories")
|
||||
|
||||
return self.get_indexer(keyarr)
|
||||
|
||||
@Appender(_index_shared_docs['_convert_arr_indexer'])
|
||||
def _convert_arr_indexer(self, keyarr):
|
||||
keyarr = com._asarray_tuplesafe(keyarr)
|
||||
|
||||
if self.categories._defer_to_indexing:
|
||||
return keyarr
|
||||
|
||||
return self._shallow_copy(keyarr)
|
||||
|
||||
@Appender(_index_shared_docs['_convert_index_indexer'])
|
||||
def _convert_index_indexer(self, keyarr):
|
||||
return self._shallow_copy(keyarr)
|
||||
|
||||
@Appender(_index_shared_docs['take'] % _index_doc_kwargs)
|
||||
def take(self, indices, axis=0, allow_fill=True,
|
||||
fill_value=None, **kwargs):
|
||||
nv.validate_take(tuple(), kwargs)
|
||||
indices = _ensure_platform_int(indices)
|
||||
taken = self._assert_take_fillable(self.codes, indices,
|
||||
allow_fill=allow_fill,
|
||||
fill_value=fill_value,
|
||||
na_value=-1)
|
||||
return self._create_from_codes(taken)
|
||||
|
||||
def is_dtype_equal(self, other):
|
||||
return self._data.is_dtype_equal(other)
|
||||
|
||||
take_nd = take
|
||||
|
||||
def map(self, mapper):
|
||||
"""
|
||||
Map values using input correspondence (a dict, Series, or function).
|
||||
|
||||
Maps the values (their categories, not the codes) of the index to new
|
||||
categories. If the mapping correspondence is one-to-one the result is a
|
||||
:class:`~pandas.CategoricalIndex` which has the same order property as
|
||||
the original, otherwise an :class:`~pandas.Index` is returned.
|
||||
|
||||
If a `dict` or :class:`~pandas.Series` is used any unmapped category is
|
||||
mapped to `NaN`. Note that if this happens an :class:`~pandas.Index`
|
||||
will be returned.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mapper : function, dict, or Series
|
||||
Mapping correspondence.
|
||||
|
||||
Returns
|
||||
-------
|
||||
pandas.CategoricalIndex or pandas.Index
|
||||
Mapped index.
|
||||
|
||||
See Also
|
||||
--------
|
||||
Index.map : Apply a mapping correspondence on an
|
||||
:class:`~pandas.Index`.
|
||||
Series.map : Apply a mapping correspondence on a
|
||||
:class:`~pandas.Series`.
|
||||
Series.apply : Apply more complex functions on a
|
||||
:class:`~pandas.Series`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> idx = pd.CategoricalIndex(['a', 'b', 'c'])
|
||||
>>> idx
|
||||
CategoricalIndex(['a', 'b', 'c'], categories=['a', 'b', 'c'],
|
||||
ordered=False, dtype='category')
|
||||
>>> idx.map(lambda x: x.upper())
|
||||
CategoricalIndex(['A', 'B', 'C'], categories=['A', 'B', 'C'],
|
||||
ordered=False, dtype='category')
|
||||
>>> idx.map({'a': 'first', 'b': 'second', 'c': 'third'})
|
||||
CategoricalIndex(['first', 'second', 'third'], categories=['first',
|
||||
'second', 'third'], ordered=False, dtype='category')
|
||||
|
||||
If the mapping is one-to-one the ordering of the categories is
|
||||
preserved:
|
||||
|
||||
>>> idx = pd.CategoricalIndex(['a', 'b', 'c'], ordered=True)
|
||||
>>> idx
|
||||
CategoricalIndex(['a', 'b', 'c'], categories=['a', 'b', 'c'],
|
||||
ordered=True, dtype='category')
|
||||
>>> idx.map({'a': 3, 'b': 2, 'c': 1})
|
||||
CategoricalIndex([3, 2, 1], categories=[3, 2, 1], ordered=True,
|
||||
dtype='category')
|
||||
|
||||
If the mapping is not one-to-one an :class:`~pandas.Index` is returned:
|
||||
|
||||
>>> idx.map({'a': 'first', 'b': 'second', 'c': 'first'})
|
||||
Index(['first', 'second', 'first'], dtype='object')
|
||||
|
||||
If a `dict` is used, all unmapped categories are mapped to `NaN` and
|
||||
the result is an :class:`~pandas.Index`:
|
||||
|
||||
>>> idx.map({'a': 'first', 'b': 'second'})
|
||||
Index(['first', 'second', nan], dtype='object')
|
||||
"""
|
||||
return self._shallow_copy_with_infer(self.values.map(mapper))
|
||||
|
||||
def delete(self, loc):
|
||||
"""
|
||||
Make new Index with passed location(-s) deleted
|
||||
|
||||
Returns
|
||||
-------
|
||||
new_index : Index
|
||||
"""
|
||||
return self._create_from_codes(np.delete(self.codes, loc))
|
||||
|
||||
def insert(self, loc, item):
|
||||
"""
|
||||
Make new Index inserting new item at location. Follows
|
||||
Python list.append semantics for negative values
|
||||
|
||||
Parameters
|
||||
----------
|
||||
loc : int
|
||||
item : object
|
||||
|
||||
Returns
|
||||
-------
|
||||
new_index : Index
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError if the item is not in the categories
|
||||
|
||||
"""
|
||||
code = self.categories.get_indexer([item])
|
||||
if (code == -1) and not (is_scalar(item) and isna(item)):
|
||||
raise TypeError("cannot insert an item into a CategoricalIndex "
|
||||
"that is not already an existing category")
|
||||
|
||||
codes = self.codes
|
||||
codes = np.concatenate((codes[:loc], code, codes[loc:]))
|
||||
return self._create_from_codes(codes)
|
||||
|
||||
def _concat(self, to_concat, name):
|
||||
# if calling index is category, don't check dtype of others
|
||||
return CategoricalIndex._concat_same_dtype(self, to_concat, name)
|
||||
|
||||
def _concat_same_dtype(self, to_concat, name):
|
||||
"""
|
||||
Concatenate to_concat which has the same class
|
||||
ValueError if other is not in the categories
|
||||
"""
|
||||
to_concat = [self._is_dtype_compat(c) for c in to_concat]
|
||||
codes = np.concatenate([c.codes for c in to_concat])
|
||||
result = self._create_from_codes(codes, name=name)
|
||||
# if name is None, _create_from_codes sets self.name
|
||||
result.name = name
|
||||
return result
|
||||
|
||||
def _codes_for_groupby(self, sort, observed):
|
||||
""" Return a Categorical adjusted for groupby """
|
||||
return self.values._codes_for_groupby(sort, observed)
|
||||
|
||||
@classmethod
|
||||
def _add_comparison_methods(cls):
|
||||
""" add in comparison methods """
|
||||
|
||||
def _make_compare(op):
|
||||
opname = '__{op}__'.format(op=op.__name__)
|
||||
|
||||
def _evaluate_compare(self, other):
|
||||
|
||||
# if we have a Categorical type, then must have the same
|
||||
# categories
|
||||
if isinstance(other, CategoricalIndex):
|
||||
other = other._values
|
||||
elif isinstance(other, Index):
|
||||
other = self._create_categorical(
|
||||
self, other._values, categories=self.categories,
|
||||
ordered=self.ordered)
|
||||
|
||||
if isinstance(other, (ABCCategorical, np.ndarray,
|
||||
ABCSeries)):
|
||||
if len(self.values) != len(other):
|
||||
raise ValueError("Lengths must match to compare")
|
||||
|
||||
if isinstance(other, ABCCategorical):
|
||||
if not self.values.is_dtype_equal(other):
|
||||
raise TypeError("categorical index comparisons must "
|
||||
"have the same categories and ordered "
|
||||
"attributes")
|
||||
|
||||
result = op(self.values, other)
|
||||
if isinstance(result, ABCSeries):
|
||||
# Dispatch to pd.Categorical returned NotImplemented
|
||||
# and we got a Series back; down-cast to ndarray
|
||||
result = result.values
|
||||
return result
|
||||
|
||||
return compat.set_function_name(_evaluate_compare, opname, cls)
|
||||
|
||||
cls.__eq__ = _make_compare(operator.eq)
|
||||
cls.__ne__ = _make_compare(operator.ne)
|
||||
cls.__lt__ = _make_compare(operator.lt)
|
||||
cls.__gt__ = _make_compare(operator.gt)
|
||||
cls.__le__ = _make_compare(operator.le)
|
||||
cls.__ge__ = _make_compare(operator.ge)
|
||||
|
||||
def _delegate_method(self, name, *args, **kwargs):
|
||||
""" method delegation to the ._values """
|
||||
method = getattr(self._values, name)
|
||||
if 'inplace' in kwargs:
|
||||
raise ValueError("cannot use inplace with CategoricalIndex")
|
||||
res = method(*args, **kwargs)
|
||||
if is_scalar(res):
|
||||
return res
|
||||
return CategoricalIndex(res, name=self.name)
|
||||
|
||||
@classmethod
|
||||
def _add_accessors(cls):
|
||||
""" add in Categorical accessor methods """
|
||||
|
||||
from pandas.core.arrays import Categorical
|
||||
CategoricalIndex._add_delegate_accessors(
|
||||
delegate=Categorical, accessors=["rename_categories",
|
||||
"reorder_categories",
|
||||
"add_categories",
|
||||
"remove_categories",
|
||||
"remove_unused_categories",
|
||||
"set_categories",
|
||||
"as_ordered", "as_unordered",
|
||||
"min", "max"],
|
||||
typ='method', overwrite=True)
|
||||
|
||||
|
||||
CategoricalIndex._add_numeric_methods_add_sub_disabled()
|
||||
CategoricalIndex._add_numeric_methods_disabled()
|
||||
CategoricalIndex._add_logical_methods_disabled()
|
||||
CategoricalIndex._add_comparison_methods()
|
||||
CategoricalIndex._add_accessors()
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,150 @@
|
||||
"""
|
||||
frozen (immutable) data structures to support MultiIndexing
|
||||
|
||||
These are used for:
|
||||
|
||||
- .names (FrozenList)
|
||||
- .levels & .labels (FrozenNDArray)
|
||||
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from pandas.core.base import PandasObject
|
||||
from pandas.core.dtypes.cast import coerce_indexer_dtype
|
||||
from pandas.io.formats.printing import pprint_thing
|
||||
|
||||
|
||||
class FrozenList(PandasObject, list):
|
||||
|
||||
"""
|
||||
Container that doesn't allow setting item *but*
|
||||
because it's technically non-hashable, will be used
|
||||
for lookups, appropriately, etc.
|
||||
"""
|
||||
# Sidenote: This has to be of type list, otherwise it messes up PyTables
|
||||
# typechecks
|
||||
|
||||
def __add__(self, other):
|
||||
if isinstance(other, tuple):
|
||||
other = list(other)
|
||||
return self.__class__(super(FrozenList, self).__add__(other))
|
||||
|
||||
__iadd__ = __add__
|
||||
|
||||
# Python 2 compat
|
||||
def __getslice__(self, i, j):
|
||||
return self.__class__(super(FrozenList, self).__getslice__(i, j))
|
||||
|
||||
def __getitem__(self, n):
|
||||
# Python 3 compat
|
||||
if isinstance(n, slice):
|
||||
return self.__class__(super(FrozenList, self).__getitem__(n))
|
||||
return super(FrozenList, self).__getitem__(n)
|
||||
|
||||
def __radd__(self, other):
|
||||
if isinstance(other, tuple):
|
||||
other = list(other)
|
||||
return self.__class__(other + list(self))
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, (tuple, FrozenList)):
|
||||
other = list(other)
|
||||
return super(FrozenList, self).__eq__(other)
|
||||
|
||||
__req__ = __eq__
|
||||
|
||||
def __mul__(self, other):
|
||||
return self.__class__(super(FrozenList, self).__mul__(other))
|
||||
|
||||
__imul__ = __mul__
|
||||
|
||||
def __reduce__(self):
|
||||
return self.__class__, (list(self),)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(tuple(self))
|
||||
|
||||
def _disabled(self, *args, **kwargs):
|
||||
"""This method will not function because object is immutable."""
|
||||
raise TypeError("'%s' does not support mutable operations." %
|
||||
self.__class__.__name__)
|
||||
|
||||
def __unicode__(self):
|
||||
return pprint_thing(self, quote_strings=True,
|
||||
escape_chars=('\t', '\r', '\n'))
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (self.__class__.__name__,
|
||||
str(self))
|
||||
|
||||
__setitem__ = __setslice__ = __delitem__ = __delslice__ = _disabled
|
||||
pop = append = extend = remove = sort = insert = _disabled
|
||||
|
||||
|
||||
class FrozenNDArray(PandasObject, np.ndarray):
|
||||
|
||||
# no __array_finalize__ for now because no metadata
|
||||
def __new__(cls, data, dtype=None, copy=False):
|
||||
if copy is None:
|
||||
copy = not isinstance(data, FrozenNDArray)
|
||||
res = np.array(data, dtype=dtype, copy=copy).view(cls)
|
||||
return res
|
||||
|
||||
def _disabled(self, *args, **kwargs):
|
||||
"""This method will not function because object is immutable."""
|
||||
raise TypeError("'%s' does not support mutable operations." %
|
||||
self.__class__)
|
||||
|
||||
__setitem__ = __setslice__ = __delitem__ = __delslice__ = _disabled
|
||||
put = itemset = fill = _disabled
|
||||
|
||||
def _shallow_copy(self):
|
||||
return self.view()
|
||||
|
||||
def values(self):
|
||||
"""returns *copy* of underlying array"""
|
||||
arr = self.view(np.ndarray).copy()
|
||||
return arr
|
||||
|
||||
def __unicode__(self):
|
||||
"""
|
||||
Return a string representation for this object.
|
||||
|
||||
Invoked by unicode(df) in py2 only. Yields a Unicode String in both
|
||||
py2/py3.
|
||||
"""
|
||||
prepr = pprint_thing(self, escape_chars=('\t', '\r', '\n'),
|
||||
quote_strings=True)
|
||||
return "%s(%s, dtype='%s')" % (type(self).__name__, prepr, self.dtype)
|
||||
|
||||
def searchsorted(self, v, side='left', sorter=None):
|
||||
"""
|
||||
Find indices where elements of v should be inserted
|
||||
in a to maintain order.
|
||||
|
||||
For full documentation, see `numpy.searchsorted`
|
||||
|
||||
See Also
|
||||
--------
|
||||
numpy.searchsorted : equivalent function
|
||||
"""
|
||||
|
||||
# we are much more performant if the searched
|
||||
# indexer is the same type as the array
|
||||
# this doesn't matter for int64, but DOES
|
||||
# matter for smaller int dtypes
|
||||
# https://github.com/numpy/numpy/issues/5370
|
||||
try:
|
||||
v = self.dtype.type(v)
|
||||
except:
|
||||
pass
|
||||
return super(FrozenNDArray, self).searchsorted(
|
||||
v, side=side, sorter=sorter)
|
||||
|
||||
|
||||
def _ensure_frozen(array_like, categories, copy=False):
|
||||
array_like = coerce_indexer_dtype(array_like, categories)
|
||||
array_like = array_like.view(FrozenNDArray)
|
||||
if copy:
|
||||
array_like = array_like.copy()
|
||||
return array_like
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,433 @@
|
||||
import numpy as np
|
||||
from pandas._libs import (index as libindex,
|
||||
join as libjoin)
|
||||
from pandas.core.dtypes.common import (
|
||||
is_dtype_equal,
|
||||
pandas_dtype,
|
||||
needs_i8_conversion,
|
||||
is_integer_dtype,
|
||||
is_bool,
|
||||
is_bool_dtype,
|
||||
is_scalar)
|
||||
|
||||
from pandas import compat
|
||||
from pandas.core import algorithms
|
||||
import pandas.core.common as com
|
||||
from pandas.core.indexes.base import (
|
||||
Index, InvalidIndexError, _index_shared_docs)
|
||||
from pandas.util._decorators import Appender, cache_readonly
|
||||
import pandas.core.dtypes.concat as _concat
|
||||
import pandas.core.indexes.base as ibase
|
||||
|
||||
|
||||
_num_index_shared_docs = dict()
|
||||
|
||||
|
||||
class NumericIndex(Index):
|
||||
"""
|
||||
Provide numeric type operations
|
||||
|
||||
This is an abstract class
|
||||
|
||||
"""
|
||||
_is_numeric_dtype = True
|
||||
|
||||
def __new__(cls, data=None, dtype=None, copy=False, name=None,
|
||||
fastpath=False):
|
||||
|
||||
if fastpath:
|
||||
return cls._simple_new(data, name=name)
|
||||
|
||||
# is_scalar, generators handled in coerce_to_ndarray
|
||||
data = cls._coerce_to_ndarray(data)
|
||||
|
||||
if issubclass(data.dtype.type, compat.string_types):
|
||||
cls._string_data_error(data)
|
||||
|
||||
if copy or not is_dtype_equal(data.dtype, cls._default_dtype):
|
||||
subarr = np.array(data, dtype=cls._default_dtype, copy=copy)
|
||||
cls._assert_safe_casting(data, subarr)
|
||||
else:
|
||||
subarr = data
|
||||
|
||||
if name is None and hasattr(data, 'name'):
|
||||
name = data.name
|
||||
return cls._simple_new(subarr, name=name)
|
||||
|
||||
@Appender(_index_shared_docs['_maybe_cast_slice_bound'])
|
||||
def _maybe_cast_slice_bound(self, label, side, kind):
|
||||
assert kind in ['ix', 'loc', 'getitem', None]
|
||||
|
||||
# we will try to coerce to integers
|
||||
return self._maybe_cast_indexer(label)
|
||||
|
||||
@Appender(_index_shared_docs['_shallow_copy'])
|
||||
def _shallow_copy(self, values=None, **kwargs):
|
||||
if values is not None and not self._can_hold_na:
|
||||
# Ensure we are not returning an Int64Index with float data:
|
||||
return self._shallow_copy_with_infer(values=values, **kwargs)
|
||||
return (super(NumericIndex, self)._shallow_copy(values=values,
|
||||
**kwargs))
|
||||
|
||||
def _convert_for_op(self, value):
|
||||
""" Convert value to be insertable to ndarray """
|
||||
|
||||
if is_bool(value) or is_bool_dtype(value):
|
||||
# force conversion to object
|
||||
# so we don't lose the bools
|
||||
raise TypeError
|
||||
|
||||
return value
|
||||
|
||||
def _convert_tolerance(self, tolerance, target):
|
||||
tolerance = np.asarray(tolerance)
|
||||
if target.size != tolerance.size and tolerance.size > 1:
|
||||
raise ValueError('list-like tolerance size must match '
|
||||
'target index size')
|
||||
if not np.issubdtype(tolerance.dtype, np.number):
|
||||
if tolerance.ndim > 0:
|
||||
raise ValueError(('tolerance argument for %s must contain '
|
||||
'numeric elements if it is list type') %
|
||||
(type(self).__name__,))
|
||||
else:
|
||||
raise ValueError(('tolerance argument for %s must be numeric '
|
||||
'if it is a scalar: %r') %
|
||||
(type(self).__name__, tolerance))
|
||||
return tolerance
|
||||
|
||||
@classmethod
|
||||
def _assert_safe_casting(cls, data, subarr):
|
||||
"""
|
||||
Subclasses need to override this only if the process of casting data
|
||||
from some accepted dtype to the internal dtype(s) bears the risk of
|
||||
truncation (e.g. float to int).
|
||||
"""
|
||||
pass
|
||||
|
||||
def _concat_same_dtype(self, indexes, name):
|
||||
return _concat._concat_index_same_dtype(indexes).rename(name)
|
||||
|
||||
@property
|
||||
def is_all_dates(self):
|
||||
"""
|
||||
Checks that all the labels are datetime objects
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
_num_index_shared_docs['class_descr'] = """
|
||||
Immutable ndarray implementing an ordered, sliceable set. The basic object
|
||||
storing axis labels for all pandas objects. %(klass)s is a special case
|
||||
of `Index` with purely %(ltype)s labels. %(extra)s
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : array-like (1-dimensional)
|
||||
dtype : NumPy dtype (default: %(dtype)s)
|
||||
copy : bool
|
||||
Make a copy of input ndarray
|
||||
name : object
|
||||
Name to be stored in the index
|
||||
|
||||
Attributes
|
||||
----------
|
||||
None
|
||||
|
||||
Methods
|
||||
-------
|
||||
None
|
||||
|
||||
Notes
|
||||
-----
|
||||
An Index instance can **only** contain hashable objects.
|
||||
|
||||
See also
|
||||
--------
|
||||
Index : The base pandas Index type
|
||||
"""
|
||||
|
||||
_int64_descr_args = dict(
|
||||
klass='Int64Index',
|
||||
ltype='integer',
|
||||
dtype='int64',
|
||||
extra=''
|
||||
)
|
||||
|
||||
|
||||
class Int64Index(NumericIndex):
|
||||
__doc__ = _num_index_shared_docs['class_descr'] % _int64_descr_args
|
||||
|
||||
_typ = 'int64index'
|
||||
_left_indexer_unique = libjoin.left_join_indexer_unique_int64
|
||||
_left_indexer = libjoin.left_join_indexer_int64
|
||||
_inner_indexer = libjoin.inner_join_indexer_int64
|
||||
_outer_indexer = libjoin.outer_join_indexer_int64
|
||||
_can_hold_na = False
|
||||
_engine_type = libindex.Int64Engine
|
||||
_default_dtype = np.int64
|
||||
|
||||
@property
|
||||
def inferred_type(self):
|
||||
"""Always 'integer' for ``Int64Index``"""
|
||||
return 'integer'
|
||||
|
||||
@property
|
||||
def asi8(self):
|
||||
# do not cache or you'll create a memory leak
|
||||
return self.values.view('i8')
|
||||
|
||||
@Appender(_index_shared_docs['_convert_scalar_indexer'])
|
||||
def _convert_scalar_indexer(self, key, kind=None):
|
||||
assert kind in ['ix', 'loc', 'getitem', 'iloc', None]
|
||||
|
||||
# don't coerce ilocs to integers
|
||||
if kind != 'iloc':
|
||||
key = self._maybe_cast_indexer(key)
|
||||
return (super(Int64Index, self)
|
||||
._convert_scalar_indexer(key, kind=kind))
|
||||
|
||||
def _wrap_joined_index(self, joined, other):
|
||||
name = self.name if self.name == other.name else None
|
||||
return Int64Index(joined, name=name)
|
||||
|
||||
@classmethod
|
||||
def _assert_safe_casting(cls, data, subarr):
|
||||
"""
|
||||
Ensure incoming data can be represented as ints.
|
||||
"""
|
||||
if not issubclass(data.dtype.type, np.signedinteger):
|
||||
if not np.array_equal(data, subarr):
|
||||
raise TypeError('Unsafe NumPy casting, you must '
|
||||
'explicitly cast')
|
||||
|
||||
|
||||
Int64Index._add_numeric_methods()
|
||||
Int64Index._add_logical_methods()
|
||||
|
||||
_uint64_descr_args = dict(
|
||||
klass='UInt64Index',
|
||||
ltype='unsigned integer',
|
||||
dtype='uint64',
|
||||
extra=''
|
||||
)
|
||||
|
||||
|
||||
class UInt64Index(NumericIndex):
|
||||
__doc__ = _num_index_shared_docs['class_descr'] % _uint64_descr_args
|
||||
|
||||
_typ = 'uint64index'
|
||||
_left_indexer_unique = libjoin.left_join_indexer_unique_uint64
|
||||
_left_indexer = libjoin.left_join_indexer_uint64
|
||||
_inner_indexer = libjoin.inner_join_indexer_uint64
|
||||
_outer_indexer = libjoin.outer_join_indexer_uint64
|
||||
_can_hold_na = False
|
||||
_engine_type = libindex.UInt64Engine
|
||||
_default_dtype = np.uint64
|
||||
|
||||
@property
|
||||
def inferred_type(self):
|
||||
"""Always 'integer' for ``UInt64Index``"""
|
||||
return 'integer'
|
||||
|
||||
@property
|
||||
def asi8(self):
|
||||
# do not cache or you'll create a memory leak
|
||||
return self.values.view('u8')
|
||||
|
||||
@Appender(_index_shared_docs['_convert_scalar_indexer'])
|
||||
def _convert_scalar_indexer(self, key, kind=None):
|
||||
assert kind in ['ix', 'loc', 'getitem', 'iloc', None]
|
||||
|
||||
# don't coerce ilocs to integers
|
||||
if kind != 'iloc':
|
||||
key = self._maybe_cast_indexer(key)
|
||||
return (super(UInt64Index, self)
|
||||
._convert_scalar_indexer(key, kind=kind))
|
||||
|
||||
@Appender(_index_shared_docs['_convert_arr_indexer'])
|
||||
def _convert_arr_indexer(self, keyarr):
|
||||
# Cast the indexer to uint64 if possible so
|
||||
# that the values returned from indexing are
|
||||
# also uint64.
|
||||
keyarr = com._asarray_tuplesafe(keyarr)
|
||||
if is_integer_dtype(keyarr):
|
||||
return com._asarray_tuplesafe(keyarr, dtype=np.uint64)
|
||||
return keyarr
|
||||
|
||||
@Appender(_index_shared_docs['_convert_index_indexer'])
|
||||
def _convert_index_indexer(self, keyarr):
|
||||
# Cast the indexer to uint64 if possible so
|
||||
# that the values returned from indexing are
|
||||
# also uint64.
|
||||
if keyarr.is_integer():
|
||||
return keyarr.astype(np.uint64)
|
||||
return keyarr
|
||||
|
||||
def _wrap_joined_index(self, joined, other):
|
||||
name = self.name if self.name == other.name else None
|
||||
return UInt64Index(joined, name=name)
|
||||
|
||||
@classmethod
|
||||
def _assert_safe_casting(cls, data, subarr):
|
||||
"""
|
||||
Ensure incoming data can be represented as uints.
|
||||
"""
|
||||
if not issubclass(data.dtype.type, np.unsignedinteger):
|
||||
if not np.array_equal(data, subarr):
|
||||
raise TypeError('Unsafe NumPy casting, you must '
|
||||
'explicitly cast')
|
||||
|
||||
|
||||
UInt64Index._add_numeric_methods()
|
||||
UInt64Index._add_logical_methods()
|
||||
|
||||
_float64_descr_args = dict(
|
||||
klass='Float64Index',
|
||||
dtype='float64',
|
||||
ltype='float',
|
||||
extra=''
|
||||
)
|
||||
|
||||
|
||||
class Float64Index(NumericIndex):
|
||||
__doc__ = _num_index_shared_docs['class_descr'] % _float64_descr_args
|
||||
|
||||
_typ = 'float64index'
|
||||
_engine_type = libindex.Float64Engine
|
||||
_left_indexer_unique = libjoin.left_join_indexer_unique_float64
|
||||
_left_indexer = libjoin.left_join_indexer_float64
|
||||
_inner_indexer = libjoin.inner_join_indexer_float64
|
||||
_outer_indexer = libjoin.outer_join_indexer_float64
|
||||
|
||||
_default_dtype = np.float64
|
||||
|
||||
@property
|
||||
def inferred_type(self):
|
||||
"""Always 'floating' for ``Float64Index``"""
|
||||
return 'floating'
|
||||
|
||||
@Appender(_index_shared_docs['astype'])
|
||||
def astype(self, dtype, copy=True):
|
||||
dtype = pandas_dtype(dtype)
|
||||
if needs_i8_conversion(dtype):
|
||||
msg = ('Cannot convert Float64Index to dtype {dtype}; integer '
|
||||
'values are required for conversion').format(dtype=dtype)
|
||||
raise TypeError(msg)
|
||||
elif is_integer_dtype(dtype) and self.hasnans:
|
||||
# GH 13149
|
||||
raise ValueError('Cannot convert NA to integer')
|
||||
return super(Float64Index, self).astype(dtype, copy=copy)
|
||||
|
||||
@Appender(_index_shared_docs['_convert_scalar_indexer'])
|
||||
def _convert_scalar_indexer(self, key, kind=None):
|
||||
assert kind in ['ix', 'loc', 'getitem', 'iloc', None]
|
||||
|
||||
if kind == 'iloc':
|
||||
return self._validate_indexer('positional', key, kind)
|
||||
|
||||
return key
|
||||
|
||||
@Appender(_index_shared_docs['_convert_slice_indexer'])
|
||||
def _convert_slice_indexer(self, key, kind=None):
|
||||
# if we are not a slice, then we are done
|
||||
if not isinstance(key, slice):
|
||||
return key
|
||||
|
||||
if kind == 'iloc':
|
||||
return super(Float64Index, self)._convert_slice_indexer(key,
|
||||
kind=kind)
|
||||
|
||||
# translate to locations
|
||||
return self.slice_indexer(key.start, key.stop, key.step, kind=kind)
|
||||
|
||||
def _format_native_types(self, na_rep='', float_format=None, decimal='.',
|
||||
quoting=None, **kwargs):
|
||||
from pandas.io.formats.format import FloatArrayFormatter
|
||||
formatter = FloatArrayFormatter(self.values, na_rep=na_rep,
|
||||
float_format=float_format,
|
||||
decimal=decimal, quoting=quoting,
|
||||
fixed_width=False)
|
||||
return formatter.get_result_as_array()
|
||||
|
||||
def get_value(self, series, key):
|
||||
""" we always want to get an index value, never a value """
|
||||
if not is_scalar(key):
|
||||
raise InvalidIndexError
|
||||
|
||||
k = com._values_from_object(key)
|
||||
loc = self.get_loc(k)
|
||||
new_values = com._values_from_object(series)[loc]
|
||||
|
||||
return new_values
|
||||
|
||||
def equals(self, other):
|
||||
"""
|
||||
Determines if two Index objects contain the same elements.
|
||||
"""
|
||||
if self is other:
|
||||
return True
|
||||
|
||||
if not isinstance(other, Index):
|
||||
return False
|
||||
|
||||
# need to compare nans locations and make sure that they are the same
|
||||
# since nans don't compare equal this is a bit tricky
|
||||
try:
|
||||
if not isinstance(other, Float64Index):
|
||||
other = self._constructor(other)
|
||||
if (not is_dtype_equal(self.dtype, other.dtype) or
|
||||
self.shape != other.shape):
|
||||
return False
|
||||
left, right = self._ndarray_values, other._ndarray_values
|
||||
return ((left == right) | (self._isnan & other._isnan)).all()
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
def __contains__(self, other):
|
||||
if super(Float64Index, self).__contains__(other):
|
||||
return True
|
||||
|
||||
try:
|
||||
# if other is a sequence this throws a ValueError
|
||||
return np.isnan(other) and self.hasnans
|
||||
except ValueError:
|
||||
try:
|
||||
return len(other) <= 1 and ibase._try_get_item(other) in self
|
||||
except TypeError:
|
||||
pass
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
@Appender(_index_shared_docs['get_loc'])
|
||||
def get_loc(self, key, method=None, tolerance=None):
|
||||
try:
|
||||
if np.all(np.isnan(key)):
|
||||
nan_idxs = self._nan_idxs
|
||||
try:
|
||||
return nan_idxs.item()
|
||||
except (ValueError, IndexError):
|
||||
# should only need to catch ValueError here but on numpy
|
||||
# 1.7 .item() can raise IndexError when NaNs are present
|
||||
if not len(nan_idxs):
|
||||
raise KeyError(key)
|
||||
return nan_idxs
|
||||
except (TypeError, NotImplementedError):
|
||||
pass
|
||||
return super(Float64Index, self).get_loc(key, method=method,
|
||||
tolerance=tolerance)
|
||||
|
||||
@cache_readonly
|
||||
def is_unique(self):
|
||||
return super(Float64Index, self).is_unique and self._nan_idxs.size < 2
|
||||
|
||||
@Appender(Index.isin.__doc__)
|
||||
def isin(self, values, level=None):
|
||||
if level is not None:
|
||||
self._validate_index_level(level)
|
||||
return algorithms.isin(np.array(self), values)
|
||||
|
||||
|
||||
Float64Index._add_numeric_methods()
|
||||
Float64Index._add_logical_methods_disabled()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,661 @@
|
||||
from sys import getsizeof
|
||||
import operator
|
||||
from datetime import timedelta
|
||||
|
||||
import numpy as np
|
||||
from pandas._libs import index as libindex
|
||||
|
||||
from pandas.core.dtypes.common import (
|
||||
is_integer,
|
||||
is_scalar,
|
||||
is_int64_dtype)
|
||||
from pandas.core.dtypes.generic import ABCSeries, ABCTimedeltaIndex
|
||||
|
||||
from pandas import compat
|
||||
from pandas.compat import lrange, range, get_range_parameters
|
||||
from pandas.compat.numpy import function as nv
|
||||
|
||||
import pandas.core.common as com
|
||||
from pandas.core import ops
|
||||
from pandas.core.indexes.base import Index, _index_shared_docs
|
||||
from pandas.util._decorators import Appender, cache_readonly
|
||||
import pandas.core.dtypes.concat as _concat
|
||||
import pandas.core.indexes.base as ibase
|
||||
|
||||
from pandas.core.indexes.numeric import Int64Index
|
||||
|
||||
|
||||
class RangeIndex(Int64Index):
|
||||
|
||||
"""
|
||||
Immutable Index implementing a monotonic integer range.
|
||||
|
||||
RangeIndex is a memory-saving special case of Int64Index limited to
|
||||
representing monotonic ranges. Using RangeIndex may in some instances
|
||||
improve computing speed.
|
||||
|
||||
This is the default index type used
|
||||
by DataFrame and Series when no explicit index is provided by the user.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
start : int (default: 0), or other RangeIndex instance.
|
||||
If int and "stop" is not given, interpreted as "stop" instead.
|
||||
stop : int (default: 0)
|
||||
step : int (default: 1)
|
||||
name : object, optional
|
||||
Name to be stored in the index
|
||||
copy : bool, default False
|
||||
Unused, accepted for homogeneity with other index types.
|
||||
|
||||
See Also
|
||||
--------
|
||||
Index : The base pandas Index type
|
||||
Int64Index : Index of int64 data
|
||||
|
||||
Attributes
|
||||
----------
|
||||
None
|
||||
|
||||
Methods
|
||||
-------
|
||||
from_range
|
||||
"""
|
||||
|
||||
_typ = 'rangeindex'
|
||||
_engine_type = libindex.Int64Engine
|
||||
|
||||
def __new__(cls, start=None, stop=None, step=None,
|
||||
dtype=None, copy=False, name=None, fastpath=False):
|
||||
|
||||
if fastpath:
|
||||
return cls._simple_new(start, stop, step, name=name)
|
||||
|
||||
cls._validate_dtype(dtype)
|
||||
|
||||
# RangeIndex
|
||||
if isinstance(start, RangeIndex):
|
||||
if name is None:
|
||||
name = start.name
|
||||
return cls._simple_new(name=name,
|
||||
**dict(start._get_data_as_items()))
|
||||
|
||||
# validate the arguments
|
||||
def _ensure_int(value, field):
|
||||
msg = ("RangeIndex(...) must be called with integers,"
|
||||
" {value} was passed for {field}")
|
||||
if not is_scalar(value):
|
||||
raise TypeError(msg.format(value=type(value).__name__,
|
||||
field=field))
|
||||
try:
|
||||
new_value = int(value)
|
||||
assert(new_value == value)
|
||||
except (TypeError, ValueError, AssertionError):
|
||||
raise TypeError(msg.format(value=type(value).__name__,
|
||||
field=field))
|
||||
|
||||
return new_value
|
||||
|
||||
if com._all_none(start, stop, step):
|
||||
msg = "RangeIndex(...) must be called with integers"
|
||||
raise TypeError(msg)
|
||||
elif start is None:
|
||||
start = 0
|
||||
else:
|
||||
start = _ensure_int(start, 'start')
|
||||
if stop is None:
|
||||
stop = start
|
||||
start = 0
|
||||
else:
|
||||
stop = _ensure_int(stop, 'stop')
|
||||
if step is None:
|
||||
step = 1
|
||||
elif step == 0:
|
||||
raise ValueError("Step must not be zero")
|
||||
else:
|
||||
step = _ensure_int(step, 'step')
|
||||
|
||||
return cls._simple_new(start, stop, step, name)
|
||||
|
||||
@classmethod
|
||||
def from_range(cls, data, name=None, dtype=None, **kwargs):
|
||||
""" create RangeIndex from a range (py3), or xrange (py2) object """
|
||||
if not isinstance(data, range):
|
||||
raise TypeError(
|
||||
'{0}(...) must be called with object coercible to a '
|
||||
'range, {1} was passed'.format(cls.__name__, repr(data)))
|
||||
|
||||
start, stop, step = get_range_parameters(data)
|
||||
return RangeIndex(start, stop, step, dtype=dtype, name=name, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _simple_new(cls, start, stop=None, step=None, name=None,
|
||||
dtype=None, **kwargs):
|
||||
result = object.__new__(cls)
|
||||
|
||||
# handle passed None, non-integers
|
||||
if start is None and stop is None:
|
||||
# empty
|
||||
start, stop, step = 0, 0, 1
|
||||
|
||||
if start is None or not is_integer(start):
|
||||
try:
|
||||
|
||||
return RangeIndex(start, stop, step, name=name, **kwargs)
|
||||
except TypeError:
|
||||
return Index(start, stop, step, name=name, **kwargs)
|
||||
|
||||
result._start = start
|
||||
result._stop = stop or 0
|
||||
result._step = step or 1
|
||||
result.name = name
|
||||
for k, v in compat.iteritems(kwargs):
|
||||
setattr(result, k, v)
|
||||
|
||||
result._reset_identity()
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _validate_dtype(dtype):
|
||||
""" require dtype to be None or int64 """
|
||||
if not (dtype is None or is_int64_dtype(dtype)):
|
||||
raise TypeError('Invalid to pass a non-int64 dtype to RangeIndex')
|
||||
|
||||
@cache_readonly
|
||||
def _constructor(self):
|
||||
""" return the class to use for construction """
|
||||
return Int64Index
|
||||
|
||||
@cache_readonly
|
||||
def _data(self):
|
||||
return np.arange(self._start, self._stop, self._step, dtype=np.int64)
|
||||
|
||||
@cache_readonly
|
||||
def _int64index(self):
|
||||
return Int64Index(self._data, name=self.name, fastpath=True)
|
||||
|
||||
def _get_data_as_items(self):
|
||||
""" return a list of tuples of start, stop, step """
|
||||
return [('start', self._start),
|
||||
('stop', self._stop),
|
||||
('step', self._step)]
|
||||
|
||||
def __reduce__(self):
|
||||
d = self._get_attributes_dict()
|
||||
d.update(dict(self._get_data_as_items()))
|
||||
return ibase._new_Index, (self.__class__, d), None
|
||||
|
||||
def _format_attrs(self):
|
||||
"""
|
||||
Return a list of tuples of the (attr, formatted_value)
|
||||
"""
|
||||
attrs = self._get_data_as_items()
|
||||
if self.name is not None:
|
||||
attrs.append(('name', ibase.default_pprint(self.name)))
|
||||
return attrs
|
||||
|
||||
def _format_data(self, name=None):
|
||||
# we are formatting thru the attributes
|
||||
return None
|
||||
|
||||
@cache_readonly
|
||||
def nbytes(self):
|
||||
"""
|
||||
Return the number of bytes in the underlying data
|
||||
On implementations where this is undetermined (PyPy)
|
||||
assume 24 bytes for each value
|
||||
"""
|
||||
return sum(getsizeof(getattr(self, v), 24) for v in
|
||||
['_start', '_stop', '_step'])
|
||||
|
||||
def memory_usage(self, deep=False):
|
||||
"""
|
||||
Memory usage of my values
|
||||
|
||||
Parameters
|
||||
----------
|
||||
deep : bool
|
||||
Introspect the data deeply, interrogate
|
||||
`object` dtypes for system-level memory consumption
|
||||
|
||||
Returns
|
||||
-------
|
||||
bytes used
|
||||
|
||||
Notes
|
||||
-----
|
||||
Memory usage does not include memory consumed by elements that
|
||||
are not components of the array if deep=False
|
||||
|
||||
See Also
|
||||
--------
|
||||
numpy.ndarray.nbytes
|
||||
"""
|
||||
return self.nbytes
|
||||
|
||||
@property
|
||||
def dtype(self):
|
||||
return np.dtype(np.int64)
|
||||
|
||||
@property
|
||||
def is_unique(self):
|
||||
""" return if the index has unique values """
|
||||
return True
|
||||
|
||||
@cache_readonly
|
||||
def is_monotonic_increasing(self):
|
||||
return self._step > 0 or len(self) <= 1
|
||||
|
||||
@cache_readonly
|
||||
def is_monotonic_decreasing(self):
|
||||
return self._step < 0 or len(self) <= 1
|
||||
|
||||
@property
|
||||
def has_duplicates(self):
|
||||
return False
|
||||
|
||||
def tolist(self):
|
||||
return lrange(self._start, self._stop, self._step)
|
||||
|
||||
@Appender(_index_shared_docs['_shallow_copy'])
|
||||
def _shallow_copy(self, values=None, **kwargs):
|
||||
if values is None:
|
||||
return RangeIndex(name=self.name, fastpath=True,
|
||||
**dict(self._get_data_as_items()))
|
||||
else:
|
||||
kwargs.setdefault('name', self.name)
|
||||
return self._int64index._shallow_copy(values, **kwargs)
|
||||
|
||||
@Appender(ibase._index_shared_docs['copy'])
|
||||
def copy(self, name=None, deep=False, dtype=None, **kwargs):
|
||||
self._validate_dtype(dtype)
|
||||
if name is None:
|
||||
name = self.name
|
||||
return RangeIndex(name=name, fastpath=True,
|
||||
**dict(self._get_data_as_items()))
|
||||
|
||||
def _minmax(self, meth):
|
||||
no_steps = len(self) - 1
|
||||
if no_steps == -1:
|
||||
return np.nan
|
||||
elif ((meth == 'min' and self._step > 0) or
|
||||
(meth == 'max' and self._step < 0)):
|
||||
return self._start
|
||||
|
||||
return self._start + self._step * no_steps
|
||||
|
||||
def min(self):
|
||||
"""The minimum value of the RangeIndex"""
|
||||
return self._minmax('min')
|
||||
|
||||
def max(self):
|
||||
"""The maximum value of the RangeIndex"""
|
||||
return self._minmax('max')
|
||||
|
||||
def argsort(self, *args, **kwargs):
|
||||
"""
|
||||
Returns the indices that would sort the index and its
|
||||
underlying data.
|
||||
|
||||
Returns
|
||||
-------
|
||||
argsorted : numpy array
|
||||
|
||||
See also
|
||||
--------
|
||||
numpy.ndarray.argsort
|
||||
"""
|
||||
nv.validate_argsort(args, kwargs)
|
||||
|
||||
if self._step > 0:
|
||||
return np.arange(len(self))
|
||||
else:
|
||||
return np.arange(len(self) - 1, -1, -1)
|
||||
|
||||
def equals(self, other):
|
||||
"""
|
||||
Determines if two Index objects contain the same elements.
|
||||
"""
|
||||
if isinstance(other, RangeIndex):
|
||||
ls = len(self)
|
||||
lo = len(other)
|
||||
return (ls == lo == 0 or
|
||||
ls == lo == 1 and
|
||||
self._start == other._start or
|
||||
ls == lo and
|
||||
self._start == other._start and
|
||||
self._step == other._step)
|
||||
|
||||
return super(RangeIndex, self).equals(other)
|
||||
|
||||
def intersection(self, other):
|
||||
"""
|
||||
Form the intersection of two Index objects. Sortedness of the result is
|
||||
not guaranteed
|
||||
|
||||
Parameters
|
||||
----------
|
||||
other : Index or array-like
|
||||
|
||||
Returns
|
||||
-------
|
||||
intersection : Index
|
||||
"""
|
||||
if not isinstance(other, RangeIndex):
|
||||
return super(RangeIndex, self).intersection(other)
|
||||
|
||||
if not len(self) or not len(other):
|
||||
return RangeIndex._simple_new(None)
|
||||
|
||||
first = self[::-1] if self._step < 0 else self
|
||||
second = other[::-1] if other._step < 0 else other
|
||||
|
||||
# check whether intervals intersect
|
||||
# deals with in- and decreasing ranges
|
||||
int_low = max(first._start, second._start)
|
||||
int_high = min(first._stop, second._stop)
|
||||
if int_high <= int_low:
|
||||
return RangeIndex._simple_new(None)
|
||||
|
||||
# Method hint: linear Diophantine equation
|
||||
# solve intersection problem
|
||||
# performance hint: for identical step sizes, could use
|
||||
# cheaper alternative
|
||||
gcd, s, t = first._extended_gcd(first._step, second._step)
|
||||
|
||||
# check whether element sets intersect
|
||||
if (first._start - second._start) % gcd:
|
||||
return RangeIndex._simple_new(None)
|
||||
|
||||
# calculate parameters for the RangeIndex describing the
|
||||
# intersection disregarding the lower bounds
|
||||
tmp_start = first._start + (second._start - first._start) * \
|
||||
first._step // gcd * s
|
||||
new_step = first._step * second._step // gcd
|
||||
new_index = RangeIndex(tmp_start, int_high, new_step, fastpath=True)
|
||||
|
||||
# adjust index to limiting interval
|
||||
new_index._start = new_index._min_fitting_element(int_low)
|
||||
|
||||
if (self._step < 0 and other._step < 0) is not (new_index._step < 0):
|
||||
new_index = new_index[::-1]
|
||||
return new_index
|
||||
|
||||
def _min_fitting_element(self, lower_limit):
|
||||
"""Returns the smallest element greater than or equal to the limit"""
|
||||
no_steps = -(-(lower_limit - self._start) // abs(self._step))
|
||||
return self._start + abs(self._step) * no_steps
|
||||
|
||||
def _max_fitting_element(self, upper_limit):
|
||||
"""Returns the largest element smaller than or equal to the limit"""
|
||||
no_steps = (upper_limit - self._start) // abs(self._step)
|
||||
return self._start + abs(self._step) * no_steps
|
||||
|
||||
def _extended_gcd(self, a, b):
|
||||
"""
|
||||
Extended Euclidean algorithms to solve Bezout's identity:
|
||||
a*x + b*y = gcd(x, y)
|
||||
Finds one particular solution for x, y: s, t
|
||||
Returns: gcd, s, t
|
||||
"""
|
||||
s, old_s = 0, 1
|
||||
t, old_t = 1, 0
|
||||
r, old_r = b, a
|
||||
while r:
|
||||
quotient = old_r // r
|
||||
old_r, r = r, old_r - quotient * r
|
||||
old_s, s = s, old_s - quotient * s
|
||||
old_t, t = t, old_t - quotient * t
|
||||
return old_r, old_s, old_t
|
||||
|
||||
def union(self, other):
|
||||
"""
|
||||
Form the union of two Index objects and sorts if possible
|
||||
|
||||
Parameters
|
||||
----------
|
||||
other : Index or array-like
|
||||
|
||||
Returns
|
||||
-------
|
||||
union : Index
|
||||
"""
|
||||
self._assert_can_do_setop(other)
|
||||
if len(other) == 0 or self.equals(other):
|
||||
return self
|
||||
if len(self) == 0:
|
||||
return other
|
||||
if isinstance(other, RangeIndex):
|
||||
start_s, step_s = self._start, self._step
|
||||
end_s = self._start + self._step * (len(self) - 1)
|
||||
start_o, step_o = other._start, other._step
|
||||
end_o = other._start + other._step * (len(other) - 1)
|
||||
if self._step < 0:
|
||||
start_s, step_s, end_s = end_s, -step_s, start_s
|
||||
if other._step < 0:
|
||||
start_o, step_o, end_o = end_o, -step_o, start_o
|
||||
if len(self) == 1 and len(other) == 1:
|
||||
step_s = step_o = abs(self._start - other._start)
|
||||
elif len(self) == 1:
|
||||
step_s = step_o
|
||||
elif len(other) == 1:
|
||||
step_o = step_s
|
||||
start_r = min(start_s, start_o)
|
||||
end_r = max(end_s, end_o)
|
||||
if step_o == step_s:
|
||||
if ((start_s - start_o) % step_s == 0 and
|
||||
(start_s - end_o) <= step_s and
|
||||
(start_o - end_s) <= step_s):
|
||||
return RangeIndex(start_r, end_r + step_s, step_s)
|
||||
if ((step_s % 2 == 0) and
|
||||
(abs(start_s - start_o) <= step_s / 2) and
|
||||
(abs(end_s - end_o) <= step_s / 2)):
|
||||
return RangeIndex(start_r, end_r + step_s / 2, step_s / 2)
|
||||
elif step_o % step_s == 0:
|
||||
if ((start_o - start_s) % step_s == 0 and
|
||||
(start_o + step_s >= start_s) and
|
||||
(end_o - step_s <= end_s)):
|
||||
return RangeIndex(start_r, end_r + step_s, step_s)
|
||||
elif step_s % step_o == 0:
|
||||
if ((start_s - start_o) % step_o == 0 and
|
||||
(start_s + step_o >= start_o) and
|
||||
(end_s - step_o <= end_o)):
|
||||
return RangeIndex(start_r, end_r + step_o, step_o)
|
||||
|
||||
return self._int64index.union(other)
|
||||
|
||||
@Appender(_index_shared_docs['join'])
|
||||
def join(self, other, how='left', level=None, return_indexers=False,
|
||||
sort=False):
|
||||
if how == 'outer' and self is not other:
|
||||
# note: could return RangeIndex in more circumstances
|
||||
return self._int64index.join(other, how, level, return_indexers,
|
||||
sort)
|
||||
|
||||
return super(RangeIndex, self).join(other, how, level, return_indexers,
|
||||
sort)
|
||||
|
||||
def _concat_same_dtype(self, indexes, name):
|
||||
return _concat._concat_rangeindex_same_dtype(indexes).rename(name)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
return the length of the RangeIndex
|
||||
"""
|
||||
return max(0, -(-(self._stop - self._start) // self._step))
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return len(self)
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""
|
||||
Conserve RangeIndex type for scalar and slice keys.
|
||||
"""
|
||||
super_getitem = super(RangeIndex, self).__getitem__
|
||||
|
||||
if is_scalar(key):
|
||||
n = int(key)
|
||||
if n != key:
|
||||
return super_getitem(key)
|
||||
if n < 0:
|
||||
n = len(self) + key
|
||||
if n < 0 or n > len(self) - 1:
|
||||
raise IndexError("index {key} is out of bounds for axis 0 "
|
||||
"with size {size}".format(key=key,
|
||||
size=len(self)))
|
||||
return self._start + n * self._step
|
||||
|
||||
if isinstance(key, slice):
|
||||
|
||||
# This is basically PySlice_GetIndicesEx, but delegation to our
|
||||
# super routines if we don't have integers
|
||||
|
||||
l = len(self)
|
||||
|
||||
# complete missing slice information
|
||||
step = 1 if key.step is None else key.step
|
||||
if key.start is None:
|
||||
start = l - 1 if step < 0 else 0
|
||||
else:
|
||||
start = key.start
|
||||
|
||||
if start < 0:
|
||||
start += l
|
||||
if start < 0:
|
||||
start = -1 if step < 0 else 0
|
||||
if start >= l:
|
||||
start = l - 1 if step < 0 else l
|
||||
|
||||
if key.stop is None:
|
||||
stop = -1 if step < 0 else l
|
||||
else:
|
||||
stop = key.stop
|
||||
|
||||
if stop < 0:
|
||||
stop += l
|
||||
if stop < 0:
|
||||
stop = -1
|
||||
if stop > l:
|
||||
stop = l
|
||||
|
||||
# delegate non-integer slices
|
||||
if (start != int(start) or
|
||||
stop != int(stop) or
|
||||
step != int(step)):
|
||||
return super_getitem(key)
|
||||
|
||||
# convert indexes to values
|
||||
start = self._start + self._step * start
|
||||
stop = self._start + self._step * stop
|
||||
step = self._step * step
|
||||
|
||||
return RangeIndex(start, stop, step, name=self.name, fastpath=True)
|
||||
|
||||
# fall back to Int64Index
|
||||
return super_getitem(key)
|
||||
|
||||
def __floordiv__(self, other):
|
||||
if is_integer(other) and other != 0:
|
||||
if (len(self) == 0 or
|
||||
self._start % other == 0 and
|
||||
self._step % other == 0):
|
||||
start = self._start // other
|
||||
step = self._step // other
|
||||
stop = start + len(self) * step
|
||||
return RangeIndex(start, stop, step, name=self.name,
|
||||
fastpath=True)
|
||||
if len(self) == 1:
|
||||
start = self._start // other
|
||||
return RangeIndex(start, start + 1, 1, name=self.name,
|
||||
fastpath=True)
|
||||
return self._int64index // other
|
||||
|
||||
@classmethod
|
||||
def _add_numeric_methods_binary(cls):
|
||||
""" add in numeric methods, specialized to RangeIndex """
|
||||
|
||||
def _make_evaluate_binop(op, step=False):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
op : callable that accepts 2 parms
|
||||
perform the binary op
|
||||
step : callable, optional, default to False
|
||||
op to apply to the step parm if not None
|
||||
if False, use the existing step
|
||||
"""
|
||||
|
||||
def _evaluate_numeric_binop(self, other):
|
||||
if isinstance(other, ABCSeries):
|
||||
return NotImplemented
|
||||
elif isinstance(other, ABCTimedeltaIndex):
|
||||
# Defer to TimedeltaIndex implementation
|
||||
return NotImplemented
|
||||
elif isinstance(other, (timedelta, np.timedelta64)):
|
||||
# GH#19333 is_integer evaluated True on timedelta64,
|
||||
# so we need to catch these explicitly
|
||||
return op(self._int64index, other)
|
||||
|
||||
other = self._validate_for_numeric_binop(other, op)
|
||||
attrs = self._get_attributes_dict()
|
||||
attrs = self._maybe_update_attributes(attrs)
|
||||
|
||||
left, right = self, other
|
||||
|
||||
try:
|
||||
# apply if we have an override
|
||||
if step:
|
||||
with np.errstate(all='ignore'):
|
||||
rstep = step(left._step, right)
|
||||
|
||||
# we don't have a representable op
|
||||
# so return a base index
|
||||
if not is_integer(rstep) or not rstep:
|
||||
raise ValueError
|
||||
|
||||
else:
|
||||
rstep = left._step
|
||||
|
||||
with np.errstate(all='ignore'):
|
||||
rstart = op(left._start, right)
|
||||
rstop = op(left._stop, right)
|
||||
|
||||
result = RangeIndex(rstart,
|
||||
rstop,
|
||||
rstep,
|
||||
**attrs)
|
||||
|
||||
# for compat with numpy / Int64Index
|
||||
# even if we can represent as a RangeIndex, return
|
||||
# as a Float64Index if we have float-like descriptors
|
||||
if not all(is_integer(x) for x in
|
||||
[rstart, rstop, rstep]):
|
||||
result = result.astype('float64')
|
||||
|
||||
return result
|
||||
|
||||
except (ValueError, TypeError, ZeroDivisionError):
|
||||
# Defer to Int64Index implementation
|
||||
return op(self._int64index, other)
|
||||
# TODO: Do attrs get handled reliably?
|
||||
|
||||
return _evaluate_numeric_binop
|
||||
|
||||
cls.__add__ = _make_evaluate_binop(operator.add)
|
||||
cls.__radd__ = _make_evaluate_binop(ops.radd)
|
||||
cls.__sub__ = _make_evaluate_binop(operator.sub)
|
||||
cls.__rsub__ = _make_evaluate_binop(ops.rsub)
|
||||
cls.__mul__ = _make_evaluate_binop(operator.mul, step=operator.mul)
|
||||
cls.__rmul__ = _make_evaluate_binop(ops.rmul, step=ops.rmul)
|
||||
cls.__truediv__ = _make_evaluate_binop(operator.truediv,
|
||||
step=operator.truediv)
|
||||
cls.__rtruediv__ = _make_evaluate_binop(ops.rtruediv,
|
||||
step=ops.rtruediv)
|
||||
if not compat.PY3:
|
||||
cls.__div__ = _make_evaluate_binop(operator.div, step=operator.div)
|
||||
cls.__rdiv__ = _make_evaluate_binop(ops.rdiv, step=ops.rdiv)
|
||||
|
||||
|
||||
RangeIndex._add_numeric_methods()
|
||||
RangeIndex._add_logical_methods()
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,812 @@
|
||||
"""
|
||||
Routines for filling missing data
|
||||
"""
|
||||
import operator
|
||||
|
||||
import numpy as np
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from pandas._libs import algos, lib
|
||||
|
||||
from pandas.compat import range, string_types
|
||||
from pandas.core.dtypes.common import (
|
||||
is_numeric_v_string_like,
|
||||
is_float_dtype,
|
||||
is_datetime64_dtype,
|
||||
is_datetime64tz_dtype,
|
||||
is_integer_dtype,
|
||||
is_scalar,
|
||||
is_integer,
|
||||
needs_i8_conversion,
|
||||
_ensure_float64)
|
||||
|
||||
from pandas.core.dtypes.cast import infer_dtype_from_array
|
||||
from pandas.core.dtypes.missing import isna
|
||||
|
||||
|
||||
def mask_missing(arr, values_to_mask):
|
||||
"""
|
||||
Return a masking array of same size/shape as arr
|
||||
with entries equaling any member of values_to_mask set to True
|
||||
"""
|
||||
dtype, values_to_mask = infer_dtype_from_array(values_to_mask)
|
||||
|
||||
try:
|
||||
values_to_mask = np.array(values_to_mask, dtype=dtype)
|
||||
|
||||
except Exception:
|
||||
values_to_mask = np.array(values_to_mask, dtype=object)
|
||||
|
||||
na_mask = isna(values_to_mask)
|
||||
nonna = values_to_mask[~na_mask]
|
||||
|
||||
mask = None
|
||||
for x in nonna:
|
||||
if mask is None:
|
||||
|
||||
# numpy elementwise comparison warning
|
||||
if is_numeric_v_string_like(arr, x):
|
||||
mask = False
|
||||
else:
|
||||
mask = arr == x
|
||||
|
||||
# if x is a string and arr is not, then we get False and we must
|
||||
# expand the mask to size arr.shape
|
||||
if is_scalar(mask):
|
||||
mask = np.zeros(arr.shape, dtype=bool)
|
||||
else:
|
||||
|
||||
# numpy elementwise comparison warning
|
||||
if is_numeric_v_string_like(arr, x):
|
||||
mask |= False
|
||||
else:
|
||||
mask |= arr == x
|
||||
|
||||
if na_mask.any():
|
||||
if mask is None:
|
||||
mask = isna(arr)
|
||||
else:
|
||||
mask |= isna(arr)
|
||||
|
||||
return mask
|
||||
|
||||
|
||||
def clean_fill_method(method, allow_nearest=False):
|
||||
# asfreq is compat for resampling
|
||||
if method in [None, 'asfreq']:
|
||||
return None
|
||||
|
||||
if isinstance(method, string_types):
|
||||
method = method.lower()
|
||||
if method == 'ffill':
|
||||
method = 'pad'
|
||||
elif method == 'bfill':
|
||||
method = 'backfill'
|
||||
|
||||
valid_methods = ['pad', 'backfill']
|
||||
expecting = 'pad (ffill) or backfill (bfill)'
|
||||
if allow_nearest:
|
||||
valid_methods.append('nearest')
|
||||
expecting = 'pad (ffill), backfill (bfill) or nearest'
|
||||
if method not in valid_methods:
|
||||
msg = ('Invalid fill method. Expecting {expecting}. Got {method}'
|
||||
.format(expecting=expecting, method=method))
|
||||
raise ValueError(msg)
|
||||
return method
|
||||
|
||||
|
||||
def clean_interp_method(method, **kwargs):
|
||||
order = kwargs.get('order')
|
||||
valid = ['linear', 'time', 'index', 'values', 'nearest', 'zero', 'slinear',
|
||||
'quadratic', 'cubic', 'barycentric', 'polynomial', 'krogh',
|
||||
'piecewise_polynomial', 'pchip', 'akima', 'spline',
|
||||
'from_derivatives']
|
||||
if method in ('spline', 'polynomial') and order is None:
|
||||
raise ValueError("You must specify the order of the spline or "
|
||||
"polynomial.")
|
||||
if method not in valid:
|
||||
raise ValueError("method must be one of {valid}. Got '{method}' "
|
||||
"instead.".format(valid=valid, method=method))
|
||||
|
||||
return method
|
||||
|
||||
|
||||
def interpolate_1d(xvalues, yvalues, method='linear', limit=None,
|
||||
limit_direction='forward', limit_area=None, fill_value=None,
|
||||
bounds_error=False, order=None, **kwargs):
|
||||
"""
|
||||
Logic for the 1-d interpolation. The result should be 1-d, inputs
|
||||
xvalues and yvalues will each be 1-d arrays of the same length.
|
||||
|
||||
Bounds_error is currently hardcoded to False since non-scipy ones don't
|
||||
take it as an argumnet.
|
||||
"""
|
||||
# Treat the original, non-scipy methods first.
|
||||
|
||||
invalid = isna(yvalues)
|
||||
valid = ~invalid
|
||||
|
||||
if not valid.any():
|
||||
# have to call np.asarray(xvalues) since xvalues could be an Index
|
||||
# which can't be mutated
|
||||
result = np.empty_like(np.asarray(xvalues), dtype=np.float64)
|
||||
result.fill(np.nan)
|
||||
return result
|
||||
|
||||
if valid.all():
|
||||
return yvalues
|
||||
|
||||
if method == 'time':
|
||||
if not getattr(xvalues, 'is_all_dates', None):
|
||||
# if not issubclass(xvalues.dtype.type, np.datetime64):
|
||||
raise ValueError('time-weighted interpolation only works '
|
||||
'on Series or DataFrames with a '
|
||||
'DatetimeIndex')
|
||||
method = 'values'
|
||||
|
||||
valid_limit_directions = ['forward', 'backward', 'both']
|
||||
limit_direction = limit_direction.lower()
|
||||
if limit_direction not in valid_limit_directions:
|
||||
msg = ('Invalid limit_direction: expecting one of {valid!r}, '
|
||||
'got {invalid!r}.')
|
||||
raise ValueError(msg.format(valid=valid_limit_directions,
|
||||
invalid=limit_direction))
|
||||
|
||||
if limit_area is not None:
|
||||
valid_limit_areas = ['inside', 'outside']
|
||||
limit_area = limit_area.lower()
|
||||
if limit_area not in valid_limit_areas:
|
||||
raise ValueError('Invalid limit_area: expecting one of {}, got '
|
||||
'{}.'.format(valid_limit_areas, limit_area))
|
||||
|
||||
# default limit is unlimited GH #16282
|
||||
if limit is None:
|
||||
# limit = len(xvalues)
|
||||
pass
|
||||
elif not is_integer(limit):
|
||||
raise ValueError('Limit must be an integer')
|
||||
elif limit < 1:
|
||||
raise ValueError('Limit must be greater than 0')
|
||||
|
||||
from pandas import Series
|
||||
ys = Series(yvalues)
|
||||
|
||||
# These are sets of index pointers to invalid values... i.e. {0, 1, etc...
|
||||
all_nans = set(np.flatnonzero(invalid))
|
||||
start_nans = set(range(ys.first_valid_index()))
|
||||
end_nans = set(range(1 + ys.last_valid_index(), len(valid)))
|
||||
mid_nans = all_nans - start_nans - end_nans
|
||||
|
||||
# Like the sets above, preserve_nans contains indices of invalid values,
|
||||
# but in this case, it is the final set of indices that need to be
|
||||
# preserved as NaN after the interpolation.
|
||||
|
||||
# For example if limit_direction='forward' then preserve_nans will
|
||||
# contain indices of NaNs at the beginning of the series, and NaNs that
|
||||
# are more than'limit' away from the prior non-NaN.
|
||||
|
||||
# set preserve_nans based on direction using _interp_limit
|
||||
if limit_direction == 'forward':
|
||||
preserve_nans = start_nans | set(_interp_limit(invalid, limit, 0))
|
||||
elif limit_direction == 'backward':
|
||||
preserve_nans = end_nans | set(_interp_limit(invalid, 0, limit))
|
||||
else:
|
||||
# both directions... just use _interp_limit
|
||||
preserve_nans = set(_interp_limit(invalid, limit, limit))
|
||||
|
||||
# if limit_area is set, add either mid or outside indices
|
||||
# to preserve_nans GH #16284
|
||||
if limit_area == 'inside':
|
||||
# preserve NaNs on the outside
|
||||
preserve_nans |= start_nans | end_nans
|
||||
elif limit_area == 'outside':
|
||||
# preserve NaNs on the inside
|
||||
preserve_nans |= mid_nans
|
||||
|
||||
# sort preserve_nans and covert to list
|
||||
preserve_nans = sorted(preserve_nans)
|
||||
|
||||
xvalues = getattr(xvalues, 'values', xvalues)
|
||||
yvalues = getattr(yvalues, 'values', yvalues)
|
||||
result = yvalues.copy()
|
||||
|
||||
if method in ['linear', 'time', 'index', 'values']:
|
||||
if method in ('values', 'index'):
|
||||
inds = np.asarray(xvalues)
|
||||
# hack for DatetimeIndex, #1646
|
||||
if needs_i8_conversion(inds.dtype.type):
|
||||
inds = inds.view(np.int64)
|
||||
if inds.dtype == np.object_:
|
||||
inds = lib.maybe_convert_objects(inds)
|
||||
else:
|
||||
inds = xvalues
|
||||
result[invalid] = np.interp(inds[invalid], inds[valid], yvalues[valid])
|
||||
result[preserve_nans] = np.nan
|
||||
return result
|
||||
|
||||
sp_methods = ['nearest', 'zero', 'slinear', 'quadratic', 'cubic',
|
||||
'barycentric', 'krogh', 'spline', 'polynomial',
|
||||
'from_derivatives', 'piecewise_polynomial', 'pchip', 'akima']
|
||||
|
||||
if method in sp_methods:
|
||||
inds = np.asarray(xvalues)
|
||||
# hack for DatetimeIndex, #1646
|
||||
if issubclass(inds.dtype.type, np.datetime64):
|
||||
inds = inds.view(np.int64)
|
||||
result[invalid] = _interpolate_scipy_wrapper(inds[valid],
|
||||
yvalues[valid],
|
||||
inds[invalid],
|
||||
method=method,
|
||||
fill_value=fill_value,
|
||||
bounds_error=bounds_error,
|
||||
order=order, **kwargs)
|
||||
result[preserve_nans] = np.nan
|
||||
return result
|
||||
|
||||
|
||||
def _interpolate_scipy_wrapper(x, y, new_x, method, fill_value=None,
|
||||
bounds_error=False, order=None, **kwargs):
|
||||
"""
|
||||
passed off to scipy.interpolate.interp1d. method is scipy's kind.
|
||||
Returns an array interpolated at new_x. Add any new methods to
|
||||
the list in _clean_interp_method
|
||||
"""
|
||||
try:
|
||||
from scipy import interpolate
|
||||
# TODO: Why is DatetimeIndex being imported here?
|
||||
from pandas import DatetimeIndex # noqa
|
||||
except ImportError:
|
||||
raise ImportError('{method} interpolation requires SciPy'
|
||||
.format(method=method))
|
||||
|
||||
new_x = np.asarray(new_x)
|
||||
|
||||
# ignores some kwargs that could be passed along.
|
||||
alt_methods = {
|
||||
'barycentric': interpolate.barycentric_interpolate,
|
||||
'krogh': interpolate.krogh_interpolate,
|
||||
'from_derivatives': _from_derivatives,
|
||||
'piecewise_polynomial': _from_derivatives,
|
||||
}
|
||||
|
||||
if getattr(x, 'is_all_dates', False):
|
||||
# GH 5975, scipy.interp1d can't hande datetime64s
|
||||
x, new_x = x._values.astype('i8'), new_x.astype('i8')
|
||||
|
||||
if method == 'pchip':
|
||||
try:
|
||||
alt_methods['pchip'] = interpolate.pchip_interpolate
|
||||
except AttributeError:
|
||||
raise ImportError("Your version of Scipy does not support "
|
||||
"PCHIP interpolation.")
|
||||
elif method == 'akima':
|
||||
try:
|
||||
from scipy.interpolate import Akima1DInterpolator # noqa
|
||||
alt_methods['akima'] = _akima_interpolate
|
||||
except ImportError:
|
||||
raise ImportError("Your version of Scipy does not support "
|
||||
"Akima interpolation.")
|
||||
|
||||
interp1d_methods = ['nearest', 'zero', 'slinear', 'quadratic', 'cubic',
|
||||
'polynomial']
|
||||
if method in interp1d_methods:
|
||||
if method == 'polynomial':
|
||||
method = order
|
||||
terp = interpolate.interp1d(x, y, kind=method, fill_value=fill_value,
|
||||
bounds_error=bounds_error)
|
||||
new_y = terp(new_x)
|
||||
elif method == 'spline':
|
||||
# GH #10633
|
||||
if not order:
|
||||
raise ValueError("order needs to be specified and greater than 0")
|
||||
terp = interpolate.UnivariateSpline(x, y, k=order, **kwargs)
|
||||
new_y = terp(new_x)
|
||||
else:
|
||||
# GH 7295: need to be able to write for some reason
|
||||
# in some circumstances: check all three
|
||||
if not x.flags.writeable:
|
||||
x = x.copy()
|
||||
if not y.flags.writeable:
|
||||
y = y.copy()
|
||||
if not new_x.flags.writeable:
|
||||
new_x = new_x.copy()
|
||||
method = alt_methods[method]
|
||||
new_y = method(x, y, new_x, **kwargs)
|
||||
return new_y
|
||||
|
||||
|
||||
def _from_derivatives(xi, yi, x, order=None, der=0, extrapolate=False):
|
||||
"""
|
||||
Convenience function for interpolate.BPoly.from_derivatives
|
||||
|
||||
Construct a piecewise polynomial in the Bernstein basis, compatible
|
||||
with the specified values and derivatives at breakpoints.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
xi : array_like
|
||||
sorted 1D array of x-coordinates
|
||||
yi : array_like or list of array-likes
|
||||
yi[i][j] is the j-th derivative known at xi[i]
|
||||
orders : None or int or array_like of ints. Default: None.
|
||||
Specifies the degree of local polynomials. If not None, some
|
||||
derivatives are ignored.
|
||||
der : int or list
|
||||
How many derivatives to extract; None for all potentially nonzero
|
||||
derivatives (that is a number equal to the number of points), or a
|
||||
list of derivatives to extract. This numberincludes the function
|
||||
value as 0th derivative.
|
||||
extrapolate : bool, optional
|
||||
Whether to extrapolate to ouf-of-bounds points based on first and last
|
||||
intervals, or to return NaNs. Default: True.
|
||||
|
||||
See Also
|
||||
--------
|
||||
scipy.interpolate.BPoly.from_derivatives
|
||||
|
||||
Returns
|
||||
-------
|
||||
y : scalar or array_like
|
||||
The result, of length R or length M or M by R,
|
||||
|
||||
"""
|
||||
import scipy
|
||||
from scipy import interpolate
|
||||
|
||||
if LooseVersion(scipy.__version__) < LooseVersion('0.18.0'):
|
||||
try:
|
||||
method = interpolate.piecewise_polynomial_interpolate
|
||||
return method(xi, yi.reshape(-1, 1), x,
|
||||
orders=order, der=der)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# return the method for compat with scipy version & backwards compat
|
||||
method = interpolate.BPoly.from_derivatives
|
||||
m = method(xi, yi.reshape(-1, 1),
|
||||
orders=order, extrapolate=extrapolate)
|
||||
|
||||
return m(x)
|
||||
|
||||
|
||||
def _akima_interpolate(xi, yi, x, der=0, axis=0):
|
||||
"""
|
||||
Convenience function for akima interpolation.
|
||||
xi and yi are arrays of values used to approximate some function f,
|
||||
with ``yi = f(xi)``.
|
||||
|
||||
See `Akima1DInterpolator` for details.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
xi : array_like
|
||||
A sorted list of x-coordinates, of length N.
|
||||
yi : array_like
|
||||
A 1-D array of real values. `yi`'s length along the interpolation
|
||||
axis must be equal to the length of `xi`. If N-D array, use axis
|
||||
parameter to select correct axis.
|
||||
x : scalar or array_like
|
||||
Of length M.
|
||||
der : int or list, optional
|
||||
How many derivatives to extract; None for all potentially
|
||||
nonzero derivatives (that is a number equal to the number
|
||||
of points), or a list of derivatives to extract. This number
|
||||
includes the function value as 0th derivative.
|
||||
axis : int, optional
|
||||
Axis in the yi array corresponding to the x-coordinate values.
|
||||
|
||||
See Also
|
||||
--------
|
||||
scipy.interpolate.Akima1DInterpolator
|
||||
|
||||
Returns
|
||||
-------
|
||||
y : scalar or array_like
|
||||
The result, of length R or length M or M by R,
|
||||
|
||||
"""
|
||||
from scipy import interpolate
|
||||
try:
|
||||
P = interpolate.Akima1DInterpolator(xi, yi, axis=axis)
|
||||
except TypeError:
|
||||
# Scipy earlier than 0.17.0 missing axis
|
||||
P = interpolate.Akima1DInterpolator(xi, yi)
|
||||
if der == 0:
|
||||
return P(x)
|
||||
elif interpolate._isscalar(der):
|
||||
return P(x, der=der)
|
||||
else:
|
||||
return [P(x, nu) for nu in der]
|
||||
|
||||
|
||||
def interpolate_2d(values, method='pad', axis=0, limit=None, fill_value=None,
|
||||
dtype=None):
|
||||
""" perform an actual interpolation of values, values will be make 2-d if
|
||||
needed fills inplace, returns the result
|
||||
"""
|
||||
|
||||
transf = (lambda x: x) if axis == 0 else (lambda x: x.T)
|
||||
|
||||
# reshape a 1 dim if needed
|
||||
ndim = values.ndim
|
||||
if values.ndim == 1:
|
||||
if axis != 0: # pragma: no cover
|
||||
raise AssertionError("cannot interpolate on a ndim == 1 with "
|
||||
"axis != 0")
|
||||
values = values.reshape(tuple((1,) + values.shape))
|
||||
|
||||
if fill_value is None:
|
||||
mask = None
|
||||
else: # todo create faster fill func without masking
|
||||
mask = mask_missing(transf(values), fill_value)
|
||||
|
||||
method = clean_fill_method(method)
|
||||
if method == 'pad':
|
||||
values = transf(pad_2d(
|
||||
transf(values), limit=limit, mask=mask, dtype=dtype))
|
||||
else:
|
||||
values = transf(backfill_2d(
|
||||
transf(values), limit=limit, mask=mask, dtype=dtype))
|
||||
|
||||
# reshape back
|
||||
if ndim == 1:
|
||||
values = values[0]
|
||||
|
||||
return values
|
||||
|
||||
|
||||
def _interp_wrapper(f, wrap_dtype, na_override=None):
|
||||
def wrapper(arr, mask, limit=None):
|
||||
view = arr.view(wrap_dtype)
|
||||
f(view, mask, limit=limit)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
_pad_1d_datetime = _interp_wrapper(algos.pad_inplace_int64, np.int64)
|
||||
_pad_2d_datetime = _interp_wrapper(algos.pad_2d_inplace_int64, np.int64)
|
||||
_backfill_1d_datetime = _interp_wrapper(algos.backfill_inplace_int64, np.int64)
|
||||
_backfill_2d_datetime = _interp_wrapper(algos.backfill_2d_inplace_int64,
|
||||
np.int64)
|
||||
|
||||
|
||||
def pad_1d(values, limit=None, mask=None, dtype=None):
|
||||
if dtype is None:
|
||||
dtype = values.dtype
|
||||
_method = None
|
||||
if is_float_dtype(values):
|
||||
name = 'pad_inplace_{name}'.format(name=dtype.name)
|
||||
_method = getattr(algos, name, None)
|
||||
elif is_datetime64_dtype(dtype) or is_datetime64tz_dtype(dtype):
|
||||
_method = _pad_1d_datetime
|
||||
elif is_integer_dtype(values):
|
||||
values = _ensure_float64(values)
|
||||
_method = algos.pad_inplace_float64
|
||||
elif values.dtype == np.object_:
|
||||
_method = algos.pad_inplace_object
|
||||
|
||||
if _method is None:
|
||||
raise ValueError('Invalid dtype for pad_1d [{name}]'
|
||||
.format(name=dtype.name))
|
||||
|
||||
if mask is None:
|
||||
mask = isna(values)
|
||||
mask = mask.view(np.uint8)
|
||||
_method(values, mask, limit=limit)
|
||||
return values
|
||||
|
||||
|
||||
def backfill_1d(values, limit=None, mask=None, dtype=None):
|
||||
if dtype is None:
|
||||
dtype = values.dtype
|
||||
_method = None
|
||||
if is_float_dtype(values):
|
||||
name = 'backfill_inplace_{name}'.format(name=dtype.name)
|
||||
_method = getattr(algos, name, None)
|
||||
elif is_datetime64_dtype(dtype) or is_datetime64tz_dtype(dtype):
|
||||
_method = _backfill_1d_datetime
|
||||
elif is_integer_dtype(values):
|
||||
values = _ensure_float64(values)
|
||||
_method = algos.backfill_inplace_float64
|
||||
elif values.dtype == np.object_:
|
||||
_method = algos.backfill_inplace_object
|
||||
|
||||
if _method is None:
|
||||
raise ValueError('Invalid dtype for backfill_1d [{name}]'
|
||||
.format(name=dtype.name))
|
||||
|
||||
if mask is None:
|
||||
mask = isna(values)
|
||||
mask = mask.view(np.uint8)
|
||||
|
||||
_method(values, mask, limit=limit)
|
||||
return values
|
||||
|
||||
|
||||
def pad_2d(values, limit=None, mask=None, dtype=None):
|
||||
if dtype is None:
|
||||
dtype = values.dtype
|
||||
_method = None
|
||||
if is_float_dtype(values):
|
||||
name = 'pad_2d_inplace_{name}'.format(name=dtype.name)
|
||||
_method = getattr(algos, name, None)
|
||||
elif is_datetime64_dtype(dtype) or is_datetime64tz_dtype(dtype):
|
||||
_method = _pad_2d_datetime
|
||||
elif is_integer_dtype(values):
|
||||
values = _ensure_float64(values)
|
||||
_method = algos.pad_2d_inplace_float64
|
||||
elif values.dtype == np.object_:
|
||||
_method = algos.pad_2d_inplace_object
|
||||
|
||||
if _method is None:
|
||||
raise ValueError('Invalid dtype for pad_2d [{name}]'
|
||||
.format(name=dtype.name))
|
||||
|
||||
if mask is None:
|
||||
mask = isna(values)
|
||||
mask = mask.view(np.uint8)
|
||||
|
||||
if np.all(values.shape):
|
||||
_method(values, mask, limit=limit)
|
||||
else:
|
||||
# for test coverage
|
||||
pass
|
||||
return values
|
||||
|
||||
|
||||
def backfill_2d(values, limit=None, mask=None, dtype=None):
|
||||
if dtype is None:
|
||||
dtype = values.dtype
|
||||
_method = None
|
||||
if is_float_dtype(values):
|
||||
name = 'backfill_2d_inplace_{name}'.format(name=dtype.name)
|
||||
_method = getattr(algos, name, None)
|
||||
elif is_datetime64_dtype(dtype) or is_datetime64tz_dtype(dtype):
|
||||
_method = _backfill_2d_datetime
|
||||
elif is_integer_dtype(values):
|
||||
values = _ensure_float64(values)
|
||||
_method = algos.backfill_2d_inplace_float64
|
||||
elif values.dtype == np.object_:
|
||||
_method = algos.backfill_2d_inplace_object
|
||||
|
||||
if _method is None:
|
||||
raise ValueError('Invalid dtype for backfill_2d [{name}]'
|
||||
.format(name=dtype.name))
|
||||
|
||||
if mask is None:
|
||||
mask = isna(values)
|
||||
mask = mask.view(np.uint8)
|
||||
|
||||
if np.all(values.shape):
|
||||
_method(values, mask, limit=limit)
|
||||
else:
|
||||
# for test coverage
|
||||
pass
|
||||
return values
|
||||
|
||||
|
||||
_fill_methods = {'pad': pad_1d, 'backfill': backfill_1d}
|
||||
|
||||
|
||||
def get_fill_func(method):
|
||||
method = clean_fill_method(method)
|
||||
return _fill_methods[method]
|
||||
|
||||
|
||||
def clean_reindex_fill_method(method):
|
||||
return clean_fill_method(method, allow_nearest=True)
|
||||
|
||||
|
||||
def fill_zeros(result, x, y, name, fill):
|
||||
"""
|
||||
if this is a reversed op, then flip x,y
|
||||
|
||||
if we have an integer value (or array in y)
|
||||
and we have 0's, fill them with the fill,
|
||||
return the result
|
||||
|
||||
mask the nan's from x
|
||||
"""
|
||||
if fill is None or is_float_dtype(result):
|
||||
return result
|
||||
|
||||
if name.startswith(('r', '__r')):
|
||||
x, y = y, x
|
||||
|
||||
is_variable_type = (hasattr(y, 'dtype') or hasattr(y, 'type'))
|
||||
is_scalar_type = is_scalar(y)
|
||||
|
||||
if not is_variable_type and not is_scalar_type:
|
||||
return result
|
||||
|
||||
if is_scalar_type:
|
||||
y = np.array(y)
|
||||
|
||||
if is_integer_dtype(y):
|
||||
|
||||
if (y == 0).any():
|
||||
|
||||
# GH 7325, mask and nans must be broadcastable (also: PR 9308)
|
||||
# Raveling and then reshaping makes np.putmask faster
|
||||
mask = ((y == 0) & ~np.isnan(result)).ravel()
|
||||
|
||||
shape = result.shape
|
||||
result = result.astype('float64', copy=False).ravel()
|
||||
|
||||
np.putmask(result, mask, fill)
|
||||
|
||||
# if we have a fill of inf, then sign it correctly
|
||||
# (GH 6178 and PR 9308)
|
||||
if np.isinf(fill):
|
||||
signs = np.sign(y if name.startswith(('r', '__r')) else x)
|
||||
negative_inf_mask = (signs.ravel() < 0) & mask
|
||||
np.putmask(result, negative_inf_mask, -fill)
|
||||
|
||||
if "floordiv" in name: # (PR 9308)
|
||||
nan_mask = ((y == 0) & (x == 0)).ravel()
|
||||
np.putmask(result, nan_mask, np.nan)
|
||||
|
||||
result = result.reshape(shape)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def mask_zero_div_zero(x, y, result, copy=False):
|
||||
"""
|
||||
Set results of 0 / 0 or 0 // 0 to np.nan, regardless of the dtypes
|
||||
of the numerator or the denominator.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : ndarray
|
||||
y : ndarray
|
||||
result : ndarray
|
||||
copy : bool (default False)
|
||||
Whether to always create a new array or try to fill in the existing
|
||||
array if possible.
|
||||
|
||||
Returns
|
||||
-------
|
||||
filled_result : ndarray
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> x = np.array([1, 0, -1], dtype=np.int64)
|
||||
>>> y = 0 # int 0; numpy behavior is different with float
|
||||
>>> result = x / y
|
||||
>>> result # raw numpy result does not fill division by zero
|
||||
array([0, 0, 0])
|
||||
>>> mask_zero_div_zero(x, y, result)
|
||||
array([ inf, nan, -inf])
|
||||
"""
|
||||
if is_scalar(y):
|
||||
y = np.array(y)
|
||||
|
||||
zmask = y == 0
|
||||
if zmask.any():
|
||||
shape = result.shape
|
||||
|
||||
nan_mask = (zmask & (x == 0)).ravel()
|
||||
neginf_mask = (zmask & (x < 0)).ravel()
|
||||
posinf_mask = (zmask & (x > 0)).ravel()
|
||||
|
||||
if nan_mask.any() or neginf_mask.any() or posinf_mask.any():
|
||||
# Fill negative/0 with -inf, positive/0 with +inf, 0/0 with NaN
|
||||
result = result.astype('float64', copy=copy).ravel()
|
||||
|
||||
np.putmask(result, nan_mask, np.nan)
|
||||
np.putmask(result, posinf_mask, np.inf)
|
||||
np.putmask(result, neginf_mask, -np.inf)
|
||||
|
||||
result = result.reshape(shape)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def dispatch_missing(op, left, right, result):
|
||||
"""
|
||||
Fill nulls caused by division by zero, casting to a diffferent dtype
|
||||
if necessary.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
op : function (operator.add, operator.div, ...)
|
||||
left : object (Index for non-reversed ops)
|
||||
right : object (Index fof reversed ops)
|
||||
result : ndarray
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : ndarray
|
||||
"""
|
||||
opstr = '__{opname}__'.format(opname=op.__name__).replace('____', '__')
|
||||
if op in [operator.truediv, operator.floordiv,
|
||||
getattr(operator, 'div', None)]:
|
||||
result = mask_zero_div_zero(left, right, result)
|
||||
elif op is operator.mod:
|
||||
result = fill_zeros(result, left, right, opstr, np.nan)
|
||||
elif op is divmod:
|
||||
res0 = mask_zero_div_zero(left, right, result[0])
|
||||
res1 = fill_zeros(result[1], left, right, opstr, np.nan)
|
||||
result = (res0, res1)
|
||||
return result
|
||||
|
||||
|
||||
def _interp_limit(invalid, fw_limit, bw_limit):
|
||||
"""
|
||||
Get indexers of values that won't be filled
|
||||
because they exceed the limits.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
invalid : boolean ndarray
|
||||
fw_limit : int or None
|
||||
forward limit to index
|
||||
bw_limit : int or None
|
||||
backward limit to index
|
||||
|
||||
Returns
|
||||
-------
|
||||
set of indexers
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is equivalent to the more readable, but slower
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for x in np.where(invalid)[0]:
|
||||
if invalid[max(0, x - fw_limit):x + bw_limit + 1].all():
|
||||
yield x
|
||||
"""
|
||||
# handle forward first; the backward direction is the same except
|
||||
# 1. operate on the reversed array
|
||||
# 2. subtract the returned indicies from N - 1
|
||||
N = len(invalid)
|
||||
f_idx = set()
|
||||
b_idx = set()
|
||||
|
||||
def inner(invalid, limit):
|
||||
limit = min(limit, N)
|
||||
windowed = _rolling_window(invalid, limit + 1).all(1)
|
||||
idx = (set(np.where(windowed)[0] + limit) |
|
||||
set(np.where((~invalid[:limit + 1]).cumsum() == 0)[0]))
|
||||
return idx
|
||||
|
||||
if fw_limit is not None:
|
||||
|
||||
if fw_limit == 0:
|
||||
f_idx = set(np.where(invalid)[0])
|
||||
else:
|
||||
f_idx = inner(invalid, fw_limit)
|
||||
|
||||
if bw_limit is not None:
|
||||
|
||||
if bw_limit == 0:
|
||||
# then we don't even need to care about backwards
|
||||
# just use forwards
|
||||
return f_idx
|
||||
else:
|
||||
b_idx = list(inner(invalid[::-1], bw_limit))
|
||||
b_idx = set(N - 1 - np.asarray(b_idx))
|
||||
if fw_limit == 0:
|
||||
return b_idx
|
||||
|
||||
return f_idx & b_idx
|
||||
|
||||
|
||||
def _rolling_window(a, window):
|
||||
"""
|
||||
[True, True, False, True, False], 2 ->
|
||||
|
||||
[
|
||||
[True, True],
|
||||
[True, False],
|
||||
[False, True],
|
||||
[True, False],
|
||||
]
|
||||
"""
|
||||
# https://stackoverflow.com/a/6811241
|
||||
shape = a.shape[:-1] + (a.shape[-1] - window + 1, window)
|
||||
strides = a.strides + (a.strides[-1],)
|
||||
return np.lib.stride_tricks.as_strided(a, shape=shape, strides=strides)
|
||||
@@ -0,0 +1,858 @@
|
||||
import itertools
|
||||
import functools
|
||||
import operator
|
||||
import warnings
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
import numpy as np
|
||||
from pandas import compat
|
||||
from pandas._libs import tslib, lib
|
||||
from pandas.core.dtypes.common import (
|
||||
_get_dtype,
|
||||
is_float, is_scalar,
|
||||
is_integer, is_complex, is_float_dtype,
|
||||
is_complex_dtype, is_integer_dtype,
|
||||
is_bool_dtype, is_object_dtype,
|
||||
is_numeric_dtype,
|
||||
is_datetime64_dtype, is_timedelta64_dtype,
|
||||
is_datetime_or_timedelta_dtype,
|
||||
is_int_or_datetime_dtype, is_any_int_dtype)
|
||||
from pandas.core.dtypes.cast import _int64_max, maybe_upcast_putmask
|
||||
from pandas.core.dtypes.missing import isna, notna, na_value_for_dtype
|
||||
from pandas.core.config import get_option
|
||||
import pandas.core.common as com
|
||||
|
||||
_BOTTLENECK_INSTALLED = False
|
||||
_MIN_BOTTLENECK_VERSION = '1.0.0'
|
||||
|
||||
try:
|
||||
import bottleneck as bn
|
||||
ver = bn.__version__
|
||||
_BOTTLENECK_INSTALLED = (LooseVersion(ver) >=
|
||||
LooseVersion(_MIN_BOTTLENECK_VERSION))
|
||||
|
||||
if not _BOTTLENECK_INSTALLED:
|
||||
warnings.warn(
|
||||
"The installed version of bottleneck {ver} is not supported "
|
||||
"in pandas and will be not be used\nThe minimum supported "
|
||||
"version is {min_ver}\n".format(
|
||||
ver=ver, min_ver=_MIN_BOTTLENECK_VERSION), UserWarning)
|
||||
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
|
||||
_USE_BOTTLENECK = False
|
||||
|
||||
|
||||
def set_use_bottleneck(v=True):
|
||||
# set/unset to use bottleneck
|
||||
global _USE_BOTTLENECK
|
||||
if _BOTTLENECK_INSTALLED:
|
||||
_USE_BOTTLENECK = v
|
||||
|
||||
|
||||
set_use_bottleneck(get_option('compute.use_bottleneck'))
|
||||
|
||||
|
||||
class disallow(object):
|
||||
|
||||
def __init__(self, *dtypes):
|
||||
super(disallow, self).__init__()
|
||||
self.dtypes = tuple(np.dtype(dtype).type for dtype in dtypes)
|
||||
|
||||
def check(self, obj):
|
||||
return hasattr(obj, 'dtype') and issubclass(obj.dtype.type,
|
||||
self.dtypes)
|
||||
|
||||
def __call__(self, f):
|
||||
@functools.wraps(f)
|
||||
def _f(*args, **kwargs):
|
||||
obj_iter = itertools.chain(args, compat.itervalues(kwargs))
|
||||
if any(self.check(obj) for obj in obj_iter):
|
||||
msg = 'reduction operation {name!r} not allowed for this dtype'
|
||||
raise TypeError(msg.format(name=f.__name__.replace('nan', '')))
|
||||
try:
|
||||
with np.errstate(invalid='ignore'):
|
||||
return f(*args, **kwargs)
|
||||
except ValueError as e:
|
||||
# we want to transform an object array
|
||||
# ValueError message to the more typical TypeError
|
||||
# e.g. this is normally a disallowed function on
|
||||
# object arrays that contain strings
|
||||
if is_object_dtype(args[0]):
|
||||
raise TypeError(e)
|
||||
raise
|
||||
|
||||
return _f
|
||||
|
||||
|
||||
class bottleneck_switch(object):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
|
||||
def __call__(self, alt):
|
||||
bn_name = alt.__name__
|
||||
|
||||
try:
|
||||
bn_func = getattr(bn, bn_name)
|
||||
except (AttributeError, NameError): # pragma: no cover
|
||||
bn_func = None
|
||||
|
||||
@functools.wraps(alt)
|
||||
def f(values, axis=None, skipna=True, **kwds):
|
||||
if len(self.kwargs) > 0:
|
||||
for k, v in compat.iteritems(self.kwargs):
|
||||
if k not in kwds:
|
||||
kwds[k] = v
|
||||
try:
|
||||
if values.size == 0 and kwds.get('min_count') is None:
|
||||
# We are empty, returning NA for our type
|
||||
# Only applies for the default `min_count` of None
|
||||
# since that affects how empty arrays are handled.
|
||||
# TODO(GH-18976) update all the nanops methods to
|
||||
# correctly handle empty inputs and remove this check.
|
||||
# It *may* just be `var`
|
||||
return _na_for_min_count(values, axis)
|
||||
|
||||
if (_USE_BOTTLENECK and skipna and
|
||||
_bn_ok_dtype(values.dtype, bn_name)):
|
||||
result = bn_func(values, axis=axis, **kwds)
|
||||
|
||||
# prefer to treat inf/-inf as NA, but must compute the func
|
||||
# twice :(
|
||||
if _has_infs(result):
|
||||
result = alt(values, axis=axis, skipna=skipna, **kwds)
|
||||
else:
|
||||
result = alt(values, axis=axis, skipna=skipna, **kwds)
|
||||
except Exception:
|
||||
try:
|
||||
result = alt(values, axis=axis, skipna=skipna, **kwds)
|
||||
except ValueError as e:
|
||||
# we want to transform an object array
|
||||
# ValueError message to the more typical TypeError
|
||||
# e.g. this is normally a disallowed function on
|
||||
# object arrays that contain strings
|
||||
|
||||
if is_object_dtype(values):
|
||||
raise TypeError(e)
|
||||
raise
|
||||
|
||||
return result
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def _bn_ok_dtype(dt, name):
|
||||
# Bottleneck chokes on datetime64
|
||||
if (not is_object_dtype(dt) and not is_datetime_or_timedelta_dtype(dt)):
|
||||
|
||||
# GH 15507
|
||||
# bottleneck does not properly upcast during the sum
|
||||
# so can overflow
|
||||
|
||||
# GH 9422
|
||||
# further we also want to preserve NaN when all elements
|
||||
# are NaN, unlinke bottleneck/numpy which consider this
|
||||
# to be 0
|
||||
if name in ['nansum', 'nanprod']:
|
||||
return False
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_infs(result):
|
||||
if isinstance(result, np.ndarray):
|
||||
if result.dtype == 'f8':
|
||||
return lib.has_infs_f8(result.ravel())
|
||||
elif result.dtype == 'f4':
|
||||
return lib.has_infs_f4(result.ravel())
|
||||
try:
|
||||
return np.isinf(result).any()
|
||||
except (TypeError, NotImplementedError):
|
||||
# if it doesn't support infs, then it can't have infs
|
||||
return False
|
||||
|
||||
|
||||
def _get_fill_value(dtype, fill_value=None, fill_value_typ=None):
|
||||
""" return the correct fill value for the dtype of the values """
|
||||
if fill_value is not None:
|
||||
return fill_value
|
||||
if _na_ok_dtype(dtype):
|
||||
if fill_value_typ is None:
|
||||
return np.nan
|
||||
else:
|
||||
if fill_value_typ == '+inf':
|
||||
return np.inf
|
||||
else:
|
||||
return -np.inf
|
||||
else:
|
||||
if fill_value_typ is None:
|
||||
return tslib.iNaT
|
||||
else:
|
||||
if fill_value_typ == '+inf':
|
||||
# need the max int here
|
||||
return _int64_max
|
||||
else:
|
||||
return tslib.iNaT
|
||||
|
||||
|
||||
def _get_values(values, skipna, fill_value=None, fill_value_typ=None,
|
||||
isfinite=False, copy=True):
|
||||
""" utility to get the values view, mask, dtype
|
||||
if necessary copy and mask using the specified fill_value
|
||||
copy = True will force the copy
|
||||
"""
|
||||
values = com._values_from_object(values)
|
||||
if isfinite:
|
||||
mask = _isfinite(values)
|
||||
else:
|
||||
mask = isna(values)
|
||||
|
||||
dtype = values.dtype
|
||||
dtype_ok = _na_ok_dtype(dtype)
|
||||
|
||||
# get our fill value (in case we need to provide an alternative
|
||||
# dtype for it)
|
||||
fill_value = _get_fill_value(dtype, fill_value=fill_value,
|
||||
fill_value_typ=fill_value_typ)
|
||||
|
||||
if skipna:
|
||||
if copy:
|
||||
values = values.copy()
|
||||
if dtype_ok:
|
||||
np.putmask(values, mask, fill_value)
|
||||
|
||||
# promote if needed
|
||||
else:
|
||||
values, changed = maybe_upcast_putmask(values, mask, fill_value)
|
||||
|
||||
elif copy:
|
||||
values = values.copy()
|
||||
|
||||
values = _view_if_needed(values)
|
||||
|
||||
# return a platform independent precision dtype
|
||||
dtype_max = dtype
|
||||
if is_integer_dtype(dtype) or is_bool_dtype(dtype):
|
||||
dtype_max = np.int64
|
||||
elif is_float_dtype(dtype):
|
||||
dtype_max = np.float64
|
||||
|
||||
return values, mask, dtype, dtype_max
|
||||
|
||||
|
||||
def _isfinite(values):
|
||||
if is_datetime_or_timedelta_dtype(values):
|
||||
return isna(values)
|
||||
if (is_complex_dtype(values) or is_float_dtype(values) or
|
||||
is_integer_dtype(values) or is_bool_dtype(values)):
|
||||
return ~np.isfinite(values)
|
||||
return ~np.isfinite(values.astype('float64'))
|
||||
|
||||
|
||||
def _na_ok_dtype(dtype):
|
||||
return not is_int_or_datetime_dtype(dtype)
|
||||
|
||||
|
||||
def _view_if_needed(values):
|
||||
if is_datetime_or_timedelta_dtype(values):
|
||||
return values.view(np.int64)
|
||||
return values
|
||||
|
||||
|
||||
def _wrap_results(result, dtype):
|
||||
""" wrap our results if needed """
|
||||
|
||||
if is_datetime64_dtype(dtype):
|
||||
if not isinstance(result, np.ndarray):
|
||||
result = tslib.Timestamp(result)
|
||||
else:
|
||||
result = result.view(dtype)
|
||||
elif is_timedelta64_dtype(dtype):
|
||||
if not isinstance(result, np.ndarray):
|
||||
|
||||
# raise if we have a timedelta64[ns] which is too large
|
||||
if np.fabs(result) > _int64_max:
|
||||
raise ValueError("overflow in timedelta operation")
|
||||
|
||||
result = tslib.Timedelta(result, unit='ns')
|
||||
else:
|
||||
result = result.astype('i8').view(dtype)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _na_for_min_count(values, axis):
|
||||
"""Return the missing value for `values`
|
||||
|
||||
Parameters
|
||||
----------
|
||||
values : ndarray
|
||||
axis : int or None
|
||||
axis for the reduction
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : scalar or ndarray
|
||||
For 1-D values, returns a scalar of the correct missing type.
|
||||
For 2-D values, returns a 1-D array where each element is missing.
|
||||
"""
|
||||
# we either return np.nan or pd.NaT
|
||||
if is_numeric_dtype(values):
|
||||
values = values.astype('float64')
|
||||
fill_value = na_value_for_dtype(values.dtype)
|
||||
|
||||
if values.ndim == 1:
|
||||
return fill_value
|
||||
else:
|
||||
result_shape = (values.shape[:axis] +
|
||||
values.shape[axis + 1:])
|
||||
result = np.empty(result_shape, dtype=values.dtype)
|
||||
result.fill(fill_value)
|
||||
return result
|
||||
|
||||
|
||||
def nanany(values, axis=None, skipna=True):
|
||||
values, mask, dtype, _ = _get_values(values, skipna, False, copy=skipna)
|
||||
return values.any(axis)
|
||||
|
||||
|
||||
def nanall(values, axis=None, skipna=True):
|
||||
values, mask, dtype, _ = _get_values(values, skipna, True, copy=skipna)
|
||||
return values.all(axis)
|
||||
|
||||
|
||||
@disallow('M8')
|
||||
def nansum(values, axis=None, skipna=True, min_count=0):
|
||||
values, mask, dtype, dtype_max = _get_values(values, skipna, 0)
|
||||
dtype_sum = dtype_max
|
||||
if is_float_dtype(dtype):
|
||||
dtype_sum = dtype
|
||||
elif is_timedelta64_dtype(dtype):
|
||||
dtype_sum = np.float64
|
||||
the_sum = values.sum(axis, dtype=dtype_sum)
|
||||
the_sum = _maybe_null_out(the_sum, axis, mask, min_count=min_count)
|
||||
|
||||
return _wrap_results(the_sum, dtype)
|
||||
|
||||
|
||||
@disallow('M8')
|
||||
@bottleneck_switch()
|
||||
def nanmean(values, axis=None, skipna=True):
|
||||
values, mask, dtype, dtype_max = _get_values(values, skipna, 0)
|
||||
|
||||
dtype_sum = dtype_max
|
||||
dtype_count = np.float64
|
||||
if is_integer_dtype(dtype) or is_timedelta64_dtype(dtype):
|
||||
dtype_sum = np.float64
|
||||
elif is_float_dtype(dtype):
|
||||
dtype_sum = dtype
|
||||
dtype_count = dtype
|
||||
count = _get_counts(mask, axis, dtype=dtype_count)
|
||||
the_sum = _ensure_numeric(values.sum(axis, dtype=dtype_sum))
|
||||
|
||||
if axis is not None and getattr(the_sum, 'ndim', False):
|
||||
the_mean = the_sum / count
|
||||
ct_mask = count == 0
|
||||
if ct_mask.any():
|
||||
the_mean[ct_mask] = np.nan
|
||||
else:
|
||||
the_mean = the_sum / count if count > 0 else np.nan
|
||||
|
||||
return _wrap_results(the_mean, dtype)
|
||||
|
||||
|
||||
@disallow('M8')
|
||||
@bottleneck_switch()
|
||||
def nanmedian(values, axis=None, skipna=True):
|
||||
|
||||
def get_median(x):
|
||||
mask = notna(x)
|
||||
if not skipna and not mask.all():
|
||||
return np.nan
|
||||
return np.nanmedian(x[mask])
|
||||
|
||||
values, mask, dtype, dtype_max = _get_values(values, skipna)
|
||||
if not is_float_dtype(values):
|
||||
values = values.astype('f8')
|
||||
values[mask] = np.nan
|
||||
|
||||
if axis is None:
|
||||
values = values.ravel()
|
||||
|
||||
notempty = values.size
|
||||
|
||||
# an array from a frame
|
||||
if values.ndim > 1:
|
||||
|
||||
# there's a non-empty array to apply over otherwise numpy raises
|
||||
if notempty:
|
||||
if not skipna:
|
||||
return _wrap_results(
|
||||
np.apply_along_axis(get_median, axis, values), dtype)
|
||||
|
||||
# fastpath for the skipna case
|
||||
return _wrap_results(np.nanmedian(values, axis), dtype)
|
||||
|
||||
# must return the correct shape, but median is not defined for the
|
||||
# empty set so return nans of shape "everything but the passed axis"
|
||||
# since "axis" is where the reduction would occur if we had a nonempty
|
||||
# array
|
||||
shp = np.array(values.shape)
|
||||
dims = np.arange(values.ndim)
|
||||
ret = np.empty(shp[dims != axis])
|
||||
ret.fill(np.nan)
|
||||
return _wrap_results(ret, dtype)
|
||||
|
||||
# otherwise return a scalar value
|
||||
return _wrap_results(get_median(values) if notempty else np.nan, dtype)
|
||||
|
||||
|
||||
def _get_counts_nanvar(mask, axis, ddof, dtype=float):
|
||||
dtype = _get_dtype(dtype)
|
||||
count = _get_counts(mask, axis, dtype=dtype)
|
||||
d = count - dtype.type(ddof)
|
||||
|
||||
# always return NaN, never inf
|
||||
if is_scalar(count):
|
||||
if count <= ddof:
|
||||
count = np.nan
|
||||
d = np.nan
|
||||
else:
|
||||
mask2 = count <= ddof
|
||||
if mask2.any():
|
||||
np.putmask(d, mask2, np.nan)
|
||||
np.putmask(count, mask2, np.nan)
|
||||
return count, d
|
||||
|
||||
|
||||
@disallow('M8')
|
||||
@bottleneck_switch(ddof=1)
|
||||
def nanstd(values, axis=None, skipna=True, ddof=1):
|
||||
result = np.sqrt(nanvar(values, axis=axis, skipna=skipna, ddof=ddof))
|
||||
return _wrap_results(result, values.dtype)
|
||||
|
||||
|
||||
@disallow('M8')
|
||||
@bottleneck_switch(ddof=1)
|
||||
def nanvar(values, axis=None, skipna=True, ddof=1):
|
||||
|
||||
values = com._values_from_object(values)
|
||||
dtype = values.dtype
|
||||
mask = isna(values)
|
||||
if is_any_int_dtype(values):
|
||||
values = values.astype('f8')
|
||||
values[mask] = np.nan
|
||||
|
||||
if is_float_dtype(values):
|
||||
count, d = _get_counts_nanvar(mask, axis, ddof, values.dtype)
|
||||
else:
|
||||
count, d = _get_counts_nanvar(mask, axis, ddof)
|
||||
|
||||
if skipna:
|
||||
values = values.copy()
|
||||
np.putmask(values, mask, 0)
|
||||
|
||||
# xref GH10242
|
||||
# Compute variance via two-pass algorithm, which is stable against
|
||||
# cancellation errors and relatively accurate for small numbers of
|
||||
# observations.
|
||||
#
|
||||
# See https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance
|
||||
avg = _ensure_numeric(values.sum(axis=axis, dtype=np.float64)) / count
|
||||
if axis is not None:
|
||||
avg = np.expand_dims(avg, axis)
|
||||
sqr = _ensure_numeric((avg - values)**2)
|
||||
np.putmask(sqr, mask, 0)
|
||||
result = sqr.sum(axis=axis, dtype=np.float64) / d
|
||||
|
||||
# Return variance as np.float64 (the datatype used in the accumulator),
|
||||
# unless we were dealing with a float array, in which case use the same
|
||||
# precision as the original values array.
|
||||
if is_float_dtype(dtype):
|
||||
result = result.astype(dtype)
|
||||
return _wrap_results(result, values.dtype)
|
||||
|
||||
|
||||
@disallow('M8', 'm8')
|
||||
def nansem(values, axis=None, skipna=True, ddof=1):
|
||||
var = nanvar(values, axis, skipna, ddof=ddof)
|
||||
|
||||
mask = isna(values)
|
||||
if not is_float_dtype(values.dtype):
|
||||
values = values.astype('f8')
|
||||
count, _ = _get_counts_nanvar(mask, axis, ddof, values.dtype)
|
||||
var = nanvar(values, axis, skipna, ddof=ddof)
|
||||
|
||||
return np.sqrt(var) / np.sqrt(count)
|
||||
|
||||
|
||||
def _nanminmax(meth, fill_value_typ):
|
||||
@bottleneck_switch()
|
||||
def reduction(values, axis=None, skipna=True):
|
||||
values, mask, dtype, dtype_max = _get_values(
|
||||
values, skipna, fill_value_typ=fill_value_typ, )
|
||||
|
||||
if ((axis is not None and values.shape[axis] == 0) or
|
||||
values.size == 0):
|
||||
try:
|
||||
result = getattr(values, meth)(axis, dtype=dtype_max)
|
||||
result.fill(np.nan)
|
||||
except:
|
||||
result = np.nan
|
||||
else:
|
||||
result = getattr(values, meth)(axis)
|
||||
|
||||
result = _wrap_results(result, dtype)
|
||||
return _maybe_null_out(result, axis, mask)
|
||||
|
||||
reduction.__name__ = 'nan' + meth
|
||||
return reduction
|
||||
|
||||
|
||||
nanmin = _nanminmax('min', fill_value_typ='+inf')
|
||||
nanmax = _nanminmax('max', fill_value_typ='-inf')
|
||||
|
||||
|
||||
@disallow('O')
|
||||
def nanargmax(values, axis=None, skipna=True):
|
||||
"""
|
||||
Returns -1 in the NA case
|
||||
"""
|
||||
values, mask, dtype, _ = _get_values(values, skipna, fill_value_typ='-inf')
|
||||
result = values.argmax(axis)
|
||||
result = _maybe_arg_null_out(result, axis, mask, skipna)
|
||||
return result
|
||||
|
||||
|
||||
@disallow('O')
|
||||
def nanargmin(values, axis=None, skipna=True):
|
||||
"""
|
||||
Returns -1 in the NA case
|
||||
"""
|
||||
values, mask, dtype, _ = _get_values(values, skipna, fill_value_typ='+inf')
|
||||
result = values.argmin(axis)
|
||||
result = _maybe_arg_null_out(result, axis, mask, skipna)
|
||||
return result
|
||||
|
||||
|
||||
@disallow('M8', 'm8')
|
||||
def nanskew(values, axis=None, skipna=True):
|
||||
""" Compute the sample skewness.
|
||||
|
||||
The statistic computed here is the adjusted Fisher-Pearson standardized
|
||||
moment coefficient G1. The algorithm computes this coefficient directly
|
||||
from the second and third central moment.
|
||||
|
||||
"""
|
||||
|
||||
values = com._values_from_object(values)
|
||||
mask = isna(values)
|
||||
if not is_float_dtype(values.dtype):
|
||||
values = values.astype('f8')
|
||||
count = _get_counts(mask, axis)
|
||||
else:
|
||||
count = _get_counts(mask, axis, dtype=values.dtype)
|
||||
|
||||
if skipna:
|
||||
values = values.copy()
|
||||
np.putmask(values, mask, 0)
|
||||
|
||||
mean = values.sum(axis, dtype=np.float64) / count
|
||||
if axis is not None:
|
||||
mean = np.expand_dims(mean, axis)
|
||||
|
||||
adjusted = values - mean
|
||||
if skipna:
|
||||
np.putmask(adjusted, mask, 0)
|
||||
adjusted2 = adjusted ** 2
|
||||
adjusted3 = adjusted2 * adjusted
|
||||
m2 = adjusted2.sum(axis, dtype=np.float64)
|
||||
m3 = adjusted3.sum(axis, dtype=np.float64)
|
||||
|
||||
# floating point error
|
||||
#
|
||||
# #18044 in _libs/windows.pyx calc_skew follow this behavior
|
||||
# to fix the fperr to treat m2 <1e-14 as zero
|
||||
m2 = _zero_out_fperr(m2)
|
||||
m3 = _zero_out_fperr(m3)
|
||||
|
||||
with np.errstate(invalid='ignore', divide='ignore'):
|
||||
result = (count * (count - 1) ** 0.5 / (count - 2)) * (m3 / m2 ** 1.5)
|
||||
|
||||
dtype = values.dtype
|
||||
if is_float_dtype(dtype):
|
||||
result = result.astype(dtype)
|
||||
|
||||
if isinstance(result, np.ndarray):
|
||||
result = np.where(m2 == 0, 0, result)
|
||||
result[count < 3] = np.nan
|
||||
return result
|
||||
else:
|
||||
result = 0 if m2 == 0 else result
|
||||
if count < 3:
|
||||
return np.nan
|
||||
return result
|
||||
|
||||
|
||||
@disallow('M8', 'm8')
|
||||
def nankurt(values, axis=None, skipna=True):
|
||||
""" Compute the sample excess kurtosis.
|
||||
|
||||
The statistic computed here is the adjusted Fisher-Pearson standardized
|
||||
moment coefficient G2, computed directly from the second and fourth
|
||||
central moment.
|
||||
|
||||
"""
|
||||
values = com._values_from_object(values)
|
||||
mask = isna(values)
|
||||
if not is_float_dtype(values.dtype):
|
||||
values = values.astype('f8')
|
||||
count = _get_counts(mask, axis)
|
||||
else:
|
||||
count = _get_counts(mask, axis, dtype=values.dtype)
|
||||
|
||||
if skipna:
|
||||
values = values.copy()
|
||||
np.putmask(values, mask, 0)
|
||||
|
||||
mean = values.sum(axis, dtype=np.float64) / count
|
||||
if axis is not None:
|
||||
mean = np.expand_dims(mean, axis)
|
||||
|
||||
adjusted = values - mean
|
||||
if skipna:
|
||||
np.putmask(adjusted, mask, 0)
|
||||
adjusted2 = adjusted ** 2
|
||||
adjusted4 = adjusted2 ** 2
|
||||
m2 = adjusted2.sum(axis, dtype=np.float64)
|
||||
m4 = adjusted4.sum(axis, dtype=np.float64)
|
||||
|
||||
with np.errstate(invalid='ignore', divide='ignore'):
|
||||
adj = 3 * (count - 1) ** 2 / ((count - 2) * (count - 3))
|
||||
numer = count * (count + 1) * (count - 1) * m4
|
||||
denom = (count - 2) * (count - 3) * m2**2
|
||||
result = numer / denom - adj
|
||||
|
||||
# floating point error
|
||||
#
|
||||
# #18044 in _libs/windows.pyx calc_kurt follow this behavior
|
||||
# to fix the fperr to treat denom <1e-14 as zero
|
||||
numer = _zero_out_fperr(numer)
|
||||
denom = _zero_out_fperr(denom)
|
||||
|
||||
if not isinstance(denom, np.ndarray):
|
||||
# if ``denom`` is a scalar, check these corner cases first before
|
||||
# doing division
|
||||
if count < 4:
|
||||
return np.nan
|
||||
if denom == 0:
|
||||
return 0
|
||||
|
||||
with np.errstate(invalid='ignore', divide='ignore'):
|
||||
result = numer / denom - adj
|
||||
|
||||
dtype = values.dtype
|
||||
if is_float_dtype(dtype):
|
||||
result = result.astype(dtype)
|
||||
|
||||
if isinstance(result, np.ndarray):
|
||||
result = np.where(denom == 0, 0, result)
|
||||
result[count < 4] = np.nan
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@disallow('M8', 'm8')
|
||||
def nanprod(values, axis=None, skipna=True, min_count=0):
|
||||
mask = isna(values)
|
||||
if skipna and not is_any_int_dtype(values):
|
||||
values = values.copy()
|
||||
values[mask] = 1
|
||||
result = values.prod(axis)
|
||||
return _maybe_null_out(result, axis, mask, min_count=min_count)
|
||||
|
||||
|
||||
def _maybe_arg_null_out(result, axis, mask, skipna):
|
||||
# helper function for nanargmin/nanargmax
|
||||
if axis is None or not getattr(result, 'ndim', False):
|
||||
if skipna:
|
||||
if mask.all():
|
||||
result = -1
|
||||
else:
|
||||
if mask.any():
|
||||
result = -1
|
||||
else:
|
||||
if skipna:
|
||||
na_mask = mask.all(axis)
|
||||
else:
|
||||
na_mask = mask.any(axis)
|
||||
if na_mask.any():
|
||||
result[na_mask] = -1
|
||||
return result
|
||||
|
||||
|
||||
def _get_counts(mask, axis, dtype=float):
|
||||
dtype = _get_dtype(dtype)
|
||||
if axis is None:
|
||||
return dtype.type(mask.size - mask.sum())
|
||||
|
||||
count = mask.shape[axis] - mask.sum(axis)
|
||||
if is_scalar(count):
|
||||
return dtype.type(count)
|
||||
try:
|
||||
return count.astype(dtype)
|
||||
except AttributeError:
|
||||
return np.array(count, dtype=dtype)
|
||||
|
||||
|
||||
def _maybe_null_out(result, axis, mask, min_count=1):
|
||||
if axis is not None and getattr(result, 'ndim', False):
|
||||
null_mask = (mask.shape[axis] - mask.sum(axis) - min_count) < 0
|
||||
if np.any(null_mask):
|
||||
if is_numeric_dtype(result):
|
||||
if np.iscomplexobj(result):
|
||||
result = result.astype('c16')
|
||||
else:
|
||||
result = result.astype('f8')
|
||||
result[null_mask] = np.nan
|
||||
else:
|
||||
# GH12941, use None to auto cast null
|
||||
result[null_mask] = None
|
||||
elif result is not tslib.NaT:
|
||||
null_mask = mask.size - mask.sum()
|
||||
if null_mask < min_count:
|
||||
result = np.nan
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _zero_out_fperr(arg):
|
||||
# #18044 reference this behavior to fix rolling skew/kurt issue
|
||||
if isinstance(arg, np.ndarray):
|
||||
with np.errstate(invalid='ignore'):
|
||||
return np.where(np.abs(arg) < 1e-14, 0, arg)
|
||||
else:
|
||||
return arg.dtype.type(0) if np.abs(arg) < 1e-14 else arg
|
||||
|
||||
|
||||
@disallow('M8', 'm8')
|
||||
def nancorr(a, b, method='pearson', min_periods=None):
|
||||
"""
|
||||
a, b: ndarrays
|
||||
"""
|
||||
if len(a) != len(b):
|
||||
raise AssertionError('Operands to nancorr must have same size')
|
||||
|
||||
if min_periods is None:
|
||||
min_periods = 1
|
||||
|
||||
valid = notna(a) & notna(b)
|
||||
if not valid.all():
|
||||
a = a[valid]
|
||||
b = b[valid]
|
||||
|
||||
if len(a) < min_periods:
|
||||
return np.nan
|
||||
|
||||
f = get_corr_func(method)
|
||||
return f(a, b)
|
||||
|
||||
|
||||
def get_corr_func(method):
|
||||
if method in ['kendall', 'spearman']:
|
||||
from scipy.stats import kendalltau, spearmanr
|
||||
|
||||
def _pearson(a, b):
|
||||
return np.corrcoef(a, b)[0, 1]
|
||||
|
||||
def _kendall(a, b):
|
||||
rs = kendalltau(a, b)
|
||||
if isinstance(rs, tuple):
|
||||
return rs[0]
|
||||
return rs
|
||||
|
||||
def _spearman(a, b):
|
||||
return spearmanr(a, b)[0]
|
||||
|
||||
_cor_methods = {
|
||||
'pearson': _pearson,
|
||||
'kendall': _kendall,
|
||||
'spearman': _spearman
|
||||
}
|
||||
return _cor_methods[method]
|
||||
|
||||
|
||||
@disallow('M8', 'm8')
|
||||
def nancov(a, b, min_periods=None):
|
||||
if len(a) != len(b):
|
||||
raise AssertionError('Operands to nancov must have same size')
|
||||
|
||||
if min_periods is None:
|
||||
min_periods = 1
|
||||
|
||||
valid = notna(a) & notna(b)
|
||||
if not valid.all():
|
||||
a = a[valid]
|
||||
b = b[valid]
|
||||
|
||||
if len(a) < min_periods:
|
||||
return np.nan
|
||||
|
||||
return np.cov(a, b)[0, 1]
|
||||
|
||||
|
||||
def _ensure_numeric(x):
|
||||
if isinstance(x, np.ndarray):
|
||||
if is_integer_dtype(x) or is_bool_dtype(x):
|
||||
x = x.astype(np.float64)
|
||||
elif is_object_dtype(x):
|
||||
try:
|
||||
x = x.astype(np.complex128)
|
||||
except:
|
||||
x = x.astype(np.float64)
|
||||
else:
|
||||
if not np.any(x.imag):
|
||||
x = x.real
|
||||
elif not (is_float(x) or is_integer(x) or is_complex(x)):
|
||||
try:
|
||||
x = float(x)
|
||||
except Exception:
|
||||
try:
|
||||
x = complex(x)
|
||||
except Exception:
|
||||
raise TypeError('Could not convert {value!s} to numeric'
|
||||
.format(value=x))
|
||||
return x
|
||||
|
||||
# NA-friendly array comparisons
|
||||
|
||||
|
||||
def make_nancomp(op):
|
||||
def f(x, y):
|
||||
xmask = isna(x)
|
||||
ymask = isna(y)
|
||||
mask = xmask | ymask
|
||||
|
||||
with np.errstate(all='ignore'):
|
||||
result = op(x, y)
|
||||
|
||||
if mask.any():
|
||||
if is_bool_dtype(result):
|
||||
result = result.astype('O')
|
||||
np.putmask(result, mask, np.nan)
|
||||
|
||||
return result
|
||||
|
||||
return f
|
||||
|
||||
|
||||
nangt = make_nancomp(operator.gt)
|
||||
nange = make_nancomp(operator.ge)
|
||||
nanlt = make_nancomp(operator.lt)
|
||||
nanle = make_nancomp(operator.le)
|
||||
naneq = make_nancomp(operator.eq)
|
||||
nanne = make_nancomp(operator.ne)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
# flake8: noqa
|
||||
|
||||
from pandas.core.reshape.concat import concat
|
||||
from pandas.core.reshape.melt import melt, lreshape, wide_to_long
|
||||
from pandas.core.reshape.reshape import pivot_simple as pivot, get_dummies
|
||||
from pandas.core.reshape.merge import merge, merge_ordered, merge_asof
|
||||
from pandas.core.reshape.pivot import pivot_table, crosstab
|
||||
from pandas.core.reshape.tile import cut, qcut
|
||||
@@ -0,0 +1,632 @@
|
||||
"""
|
||||
concat routines
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from pandas import compat, DataFrame, Series, Index, MultiIndex
|
||||
from pandas.core.index import (_get_objs_combined_axis,
|
||||
_ensure_index, _get_consensus_names,
|
||||
_all_indexes_same)
|
||||
from pandas.core.arrays.categorical import (_factorize_from_iterable,
|
||||
_factorize_from_iterables)
|
||||
from pandas.core.internals import concatenate_block_managers
|
||||
from pandas.core import common as com
|
||||
from pandas.core.generic import NDFrame
|
||||
import pandas.core.dtypes.concat as _concat
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Concatenate DataFrame objects
|
||||
|
||||
|
||||
def concat(objs, axis=0, join='outer', join_axes=None, ignore_index=False,
|
||||
keys=None, levels=None, names=None, verify_integrity=False,
|
||||
sort=None, copy=True):
|
||||
"""
|
||||
Concatenate pandas objects along a particular axis with optional set logic
|
||||
along the other axes.
|
||||
|
||||
Can also add a layer of hierarchical indexing on the concatenation axis,
|
||||
which may be useful if the labels are the same (or overlapping) on
|
||||
the passed axis number.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
objs : a sequence or mapping of Series, DataFrame, or Panel objects
|
||||
If a dict is passed, the sorted keys will be used as the `keys`
|
||||
argument, unless it is passed, in which case the values will be
|
||||
selected (see below). Any None objects will be dropped silently unless
|
||||
they are all None in which case a ValueError will be raised
|
||||
axis : {0/'index', 1/'columns'}, default 0
|
||||
The axis to concatenate along
|
||||
join : {'inner', 'outer'}, default 'outer'
|
||||
How to handle indexes on other axis(es)
|
||||
join_axes : list of Index objects
|
||||
Specific indexes to use for the other n - 1 axes instead of performing
|
||||
inner/outer set logic
|
||||
ignore_index : boolean, default False
|
||||
If True, do not use the index values along the concatenation axis. The
|
||||
resulting axis will be labeled 0, ..., n - 1. This is useful if you are
|
||||
concatenating objects where the concatenation axis does not have
|
||||
meaningful indexing information. Note the index values on the other
|
||||
axes are still respected in the join.
|
||||
keys : sequence, default None
|
||||
If multiple levels passed, should contain tuples. Construct
|
||||
hierarchical index using the passed keys as the outermost level
|
||||
levels : list of sequences, default None
|
||||
Specific levels (unique values) to use for constructing a
|
||||
MultiIndex. Otherwise they will be inferred from the keys
|
||||
names : list, default None
|
||||
Names for the levels in the resulting hierarchical index
|
||||
verify_integrity : boolean, default False
|
||||
Check whether the new concatenated axis contains duplicates. This can
|
||||
be very expensive relative to the actual data concatenation
|
||||
sort : boolean, default None
|
||||
Sort non-concatenation axis if it is not already aligned when `join`
|
||||
is 'outer'. The current default of sorting is deprecated and will
|
||||
change to not-sorting in a future version of pandas.
|
||||
|
||||
Explicitly pass ``sort=True`` to silence the warning and sort.
|
||||
Explicitly pass ``sort=False`` to silence the warning and not sort.
|
||||
|
||||
This has no effect when ``join='inner'``, which already preserves
|
||||
the order of the non-concatenation axis.
|
||||
|
||||
.. versionadded:: 0.23.0
|
||||
|
||||
copy : boolean, default True
|
||||
If False, do not copy data unnecessarily
|
||||
|
||||
Returns
|
||||
-------
|
||||
concatenated : object, type of objs
|
||||
When concatenating all ``Series`` along the index (axis=0), a
|
||||
``Series`` is returned. When ``objs`` contains at least one
|
||||
``DataFrame``, a ``DataFrame`` is returned. When concatenating along
|
||||
the columns (axis=1), a ``DataFrame`` is returned.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The keys, levels, and names arguments are all optional.
|
||||
|
||||
A walkthrough of how this method fits in with other tools for combining
|
||||
pandas objects can be found `here
|
||||
<http://pandas.pydata.org/pandas-docs/stable/merging.html>`__.
|
||||
|
||||
See Also
|
||||
--------
|
||||
Series.append
|
||||
DataFrame.append
|
||||
DataFrame.join
|
||||
DataFrame.merge
|
||||
|
||||
Examples
|
||||
--------
|
||||
Combine two ``Series``.
|
||||
|
||||
>>> s1 = pd.Series(['a', 'b'])
|
||||
>>> s2 = pd.Series(['c', 'd'])
|
||||
>>> pd.concat([s1, s2])
|
||||
0 a
|
||||
1 b
|
||||
0 c
|
||||
1 d
|
||||
dtype: object
|
||||
|
||||
Clear the existing index and reset it in the result
|
||||
by setting the ``ignore_index`` option to ``True``.
|
||||
|
||||
>>> pd.concat([s1, s2], ignore_index=True)
|
||||
0 a
|
||||
1 b
|
||||
2 c
|
||||
3 d
|
||||
dtype: object
|
||||
|
||||
Add a hierarchical index at the outermost level of
|
||||
the data with the ``keys`` option.
|
||||
|
||||
>>> pd.concat([s1, s2], keys=['s1', 's2',])
|
||||
s1 0 a
|
||||
1 b
|
||||
s2 0 c
|
||||
1 d
|
||||
dtype: object
|
||||
|
||||
Label the index keys you create with the ``names`` option.
|
||||
|
||||
>>> pd.concat([s1, s2], keys=['s1', 's2'],
|
||||
... names=['Series name', 'Row ID'])
|
||||
Series name Row ID
|
||||
s1 0 a
|
||||
1 b
|
||||
s2 0 c
|
||||
1 d
|
||||
dtype: object
|
||||
|
||||
Combine two ``DataFrame`` objects with identical columns.
|
||||
|
||||
>>> df1 = pd.DataFrame([['a', 1], ['b', 2]],
|
||||
... columns=['letter', 'number'])
|
||||
>>> df1
|
||||
letter number
|
||||
0 a 1
|
||||
1 b 2
|
||||
>>> df2 = pd.DataFrame([['c', 3], ['d', 4]],
|
||||
... columns=['letter', 'number'])
|
||||
>>> df2
|
||||
letter number
|
||||
0 c 3
|
||||
1 d 4
|
||||
>>> pd.concat([df1, df2])
|
||||
letter number
|
||||
0 a 1
|
||||
1 b 2
|
||||
0 c 3
|
||||
1 d 4
|
||||
|
||||
Combine ``DataFrame`` objects with overlapping columns
|
||||
and return everything. Columns outside the intersection will
|
||||
be filled with ``NaN`` values.
|
||||
|
||||
>>> df3 = pd.DataFrame([['c', 3, 'cat'], ['d', 4, 'dog']],
|
||||
... columns=['letter', 'number', 'animal'])
|
||||
>>> df3
|
||||
letter number animal
|
||||
0 c 3 cat
|
||||
1 d 4 dog
|
||||
>>> pd.concat([df1, df3])
|
||||
animal letter number
|
||||
0 NaN a 1
|
||||
1 NaN b 2
|
||||
0 cat c 3
|
||||
1 dog d 4
|
||||
|
||||
Combine ``DataFrame`` objects with overlapping columns
|
||||
and return only those that are shared by passing ``inner`` to
|
||||
the ``join`` keyword argument.
|
||||
|
||||
>>> pd.concat([df1, df3], join="inner")
|
||||
letter number
|
||||
0 a 1
|
||||
1 b 2
|
||||
0 c 3
|
||||
1 d 4
|
||||
|
||||
Combine ``DataFrame`` objects horizontally along the x axis by
|
||||
passing in ``axis=1``.
|
||||
|
||||
>>> df4 = pd.DataFrame([['bird', 'polly'], ['monkey', 'george']],
|
||||
... columns=['animal', 'name'])
|
||||
>>> pd.concat([df1, df4], axis=1)
|
||||
letter number animal name
|
||||
0 a 1 bird polly
|
||||
1 b 2 monkey george
|
||||
|
||||
Prevent the result from including duplicate index values with the
|
||||
``verify_integrity`` option.
|
||||
|
||||
>>> df5 = pd.DataFrame([1], index=['a'])
|
||||
>>> df5
|
||||
0
|
||||
a 1
|
||||
>>> df6 = pd.DataFrame([2], index=['a'])
|
||||
>>> df6
|
||||
0
|
||||
a 2
|
||||
>>> pd.concat([df5, df6], verify_integrity=True)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: Indexes have overlapping values: ['a']
|
||||
"""
|
||||
op = _Concatenator(objs, axis=axis, join_axes=join_axes,
|
||||
ignore_index=ignore_index, join=join,
|
||||
keys=keys, levels=levels, names=names,
|
||||
verify_integrity=verify_integrity,
|
||||
copy=copy, sort=sort)
|
||||
return op.get_result()
|
||||
|
||||
|
||||
class _Concatenator(object):
|
||||
"""
|
||||
Orchestrates a concatenation operation for BlockManagers
|
||||
"""
|
||||
|
||||
def __init__(self, objs, axis=0, join='outer', join_axes=None,
|
||||
keys=None, levels=None, names=None,
|
||||
ignore_index=False, verify_integrity=False, copy=True,
|
||||
sort=False):
|
||||
if isinstance(objs, (NDFrame, compat.string_types)):
|
||||
raise TypeError('first argument must be an iterable of pandas '
|
||||
'objects, you passed an object of type '
|
||||
'"{name}"'.format(name=type(objs).__name__))
|
||||
|
||||
if join == 'outer':
|
||||
self.intersect = False
|
||||
elif join == 'inner':
|
||||
self.intersect = True
|
||||
else: # pragma: no cover
|
||||
raise ValueError('Only can inner (intersect) or outer (union) '
|
||||
'join the other axis')
|
||||
|
||||
if isinstance(objs, dict):
|
||||
if keys is None:
|
||||
keys = sorted(objs)
|
||||
objs = [objs[k] for k in keys]
|
||||
else:
|
||||
objs = list(objs)
|
||||
|
||||
if len(objs) == 0:
|
||||
raise ValueError('No objects to concatenate')
|
||||
|
||||
if keys is None:
|
||||
objs = list(com._not_none(*objs))
|
||||
else:
|
||||
# #1649
|
||||
clean_keys = []
|
||||
clean_objs = []
|
||||
for k, v in zip(keys, objs):
|
||||
if v is None:
|
||||
continue
|
||||
clean_keys.append(k)
|
||||
clean_objs.append(v)
|
||||
objs = clean_objs
|
||||
name = getattr(keys, 'name', None)
|
||||
keys = Index(clean_keys, name=name)
|
||||
|
||||
if len(objs) == 0:
|
||||
raise ValueError('All objects passed were None')
|
||||
|
||||
# consolidate data & figure out what our result ndim is going to be
|
||||
ndims = set()
|
||||
for obj in objs:
|
||||
if not isinstance(obj, NDFrame):
|
||||
msg = ('cannot concatenate object of type "{0}";'
|
||||
' only pd.Series, pd.DataFrame, and pd.Panel'
|
||||
' (deprecated) objs are valid'.format(type(obj)))
|
||||
raise TypeError(msg)
|
||||
|
||||
# consolidate
|
||||
obj._consolidate(inplace=True)
|
||||
ndims.add(obj.ndim)
|
||||
|
||||
# get the sample
|
||||
# want the highest ndim that we have, and must be non-empty
|
||||
# unless all objs are empty
|
||||
sample = None
|
||||
if len(ndims) > 1:
|
||||
max_ndim = max(ndims)
|
||||
for obj in objs:
|
||||
if obj.ndim == max_ndim and np.sum(obj.shape):
|
||||
sample = obj
|
||||
break
|
||||
|
||||
else:
|
||||
# filter out the empties if we have not multi-index possibilities
|
||||
# note to keep empty Series as it affect to result columns / name
|
||||
non_empties = [obj for obj in objs
|
||||
if sum(obj.shape) > 0 or isinstance(obj, Series)]
|
||||
|
||||
if (len(non_empties) and (keys is None and names is None and
|
||||
levels is None and
|
||||
join_axes is None and
|
||||
not self.intersect)):
|
||||
objs = non_empties
|
||||
sample = objs[0]
|
||||
|
||||
if sample is None:
|
||||
sample = objs[0]
|
||||
self.objs = objs
|
||||
|
||||
# Standardize axis parameter to int
|
||||
if isinstance(sample, Series):
|
||||
axis = DataFrame()._get_axis_number(axis)
|
||||
else:
|
||||
axis = sample._get_axis_number(axis)
|
||||
|
||||
# Need to flip BlockManager axis in the DataFrame special case
|
||||
self._is_frame = isinstance(sample, DataFrame)
|
||||
if self._is_frame:
|
||||
axis = 1 if axis == 0 else 0
|
||||
|
||||
self._is_series = isinstance(sample, Series)
|
||||
if not 0 <= axis <= sample.ndim:
|
||||
raise AssertionError("axis must be between 0 and {ndim}, input was"
|
||||
" {axis}".format(ndim=sample.ndim, axis=axis))
|
||||
|
||||
# if we have mixed ndims, then convert to highest ndim
|
||||
# creating column numbers as needed
|
||||
if len(ndims) > 1:
|
||||
current_column = 0
|
||||
max_ndim = sample.ndim
|
||||
self.objs, objs = [], self.objs
|
||||
for obj in objs:
|
||||
|
||||
ndim = obj.ndim
|
||||
if ndim == max_ndim:
|
||||
pass
|
||||
|
||||
elif ndim != max_ndim - 1:
|
||||
raise ValueError("cannot concatenate unaligned mixed "
|
||||
"dimensional NDFrame objects")
|
||||
|
||||
else:
|
||||
name = getattr(obj, 'name', None)
|
||||
if ignore_index or name is None:
|
||||
name = current_column
|
||||
current_column += 1
|
||||
|
||||
# doing a row-wise concatenation so need everything
|
||||
# to line up
|
||||
if self._is_frame and axis == 1:
|
||||
name = 0
|
||||
obj = sample._constructor({name: obj})
|
||||
|
||||
self.objs.append(obj)
|
||||
|
||||
# note: this is the BlockManager axis (since DataFrame is transposed)
|
||||
self.axis = axis
|
||||
self.join_axes = join_axes
|
||||
self.keys = keys
|
||||
self.names = names or getattr(keys, 'names', None)
|
||||
self.levels = levels
|
||||
self.sort = sort
|
||||
|
||||
self.ignore_index = ignore_index
|
||||
self.verify_integrity = verify_integrity
|
||||
self.copy = copy
|
||||
|
||||
self.new_axes = self._get_new_axes()
|
||||
|
||||
def get_result(self):
|
||||
|
||||
# series only
|
||||
if self._is_series:
|
||||
|
||||
# stack blocks
|
||||
if self.axis == 0:
|
||||
name = com._consensus_name_attr(self.objs)
|
||||
|
||||
mgr = self.objs[0]._data.concat([x._data for x in self.objs],
|
||||
self.new_axes)
|
||||
cons = _concat._get_series_result_type(mgr, self.objs)
|
||||
return cons(mgr, name=name).__finalize__(self, method='concat')
|
||||
|
||||
# combine as columns in a frame
|
||||
else:
|
||||
data = dict(zip(range(len(self.objs)), self.objs))
|
||||
cons = _concat._get_series_result_type(data)
|
||||
|
||||
index, columns = self.new_axes
|
||||
df = cons(data, index=index)
|
||||
df.columns = columns
|
||||
return df.__finalize__(self, method='concat')
|
||||
|
||||
# combine block managers
|
||||
else:
|
||||
mgrs_indexers = []
|
||||
for obj in self.objs:
|
||||
mgr = obj._data
|
||||
indexers = {}
|
||||
for ax, new_labels in enumerate(self.new_axes):
|
||||
if ax == self.axis:
|
||||
# Suppress reindexing on concat axis
|
||||
continue
|
||||
|
||||
obj_labels = mgr.axes[ax]
|
||||
if not new_labels.equals(obj_labels):
|
||||
indexers[ax] = obj_labels.reindex(new_labels)[1]
|
||||
|
||||
mgrs_indexers.append((obj._data, indexers))
|
||||
|
||||
new_data = concatenate_block_managers(
|
||||
mgrs_indexers, self.new_axes, concat_axis=self.axis,
|
||||
copy=self.copy)
|
||||
if not self.copy:
|
||||
new_data._consolidate_inplace()
|
||||
|
||||
cons = _concat._get_frame_result_type(new_data, self.objs)
|
||||
return (cons._from_axes(new_data, self.new_axes)
|
||||
.__finalize__(self, method='concat'))
|
||||
|
||||
def _get_result_dim(self):
|
||||
if self._is_series and self.axis == 1:
|
||||
return 2
|
||||
else:
|
||||
return self.objs[0].ndim
|
||||
|
||||
def _get_new_axes(self):
|
||||
ndim = self._get_result_dim()
|
||||
new_axes = [None] * ndim
|
||||
|
||||
if self.join_axes is None:
|
||||
for i in range(ndim):
|
||||
if i == self.axis:
|
||||
continue
|
||||
new_axes[i] = self._get_comb_axis(i)
|
||||
else:
|
||||
if len(self.join_axes) != ndim - 1:
|
||||
raise AssertionError("length of join_axes must not be equal "
|
||||
"to {length}".format(length=ndim - 1))
|
||||
|
||||
# ufff...
|
||||
indices = compat.lrange(ndim)
|
||||
indices.remove(self.axis)
|
||||
|
||||
for i, ax in zip(indices, self.join_axes):
|
||||
new_axes[i] = ax
|
||||
|
||||
new_axes[self.axis] = self._get_concat_axis()
|
||||
return new_axes
|
||||
|
||||
def _get_comb_axis(self, i):
|
||||
data_axis = self.objs[0]._get_block_manager_axis(i)
|
||||
try:
|
||||
return _get_objs_combined_axis(self.objs, axis=data_axis,
|
||||
intersect=self.intersect,
|
||||
sort=self.sort)
|
||||
except IndexError:
|
||||
types = [type(x).__name__ for x in self.objs]
|
||||
raise TypeError("Cannot concatenate list of {types}"
|
||||
.format(types=types))
|
||||
|
||||
def _get_concat_axis(self):
|
||||
"""
|
||||
Return index to be used along concatenation axis.
|
||||
"""
|
||||
if self._is_series:
|
||||
if self.axis == 0:
|
||||
indexes = [x.index for x in self.objs]
|
||||
elif self.ignore_index:
|
||||
idx = com._default_index(len(self.objs))
|
||||
return idx
|
||||
elif self.keys is None:
|
||||
names = [None] * len(self.objs)
|
||||
num = 0
|
||||
has_names = False
|
||||
for i, x in enumerate(self.objs):
|
||||
if not isinstance(x, Series):
|
||||
raise TypeError("Cannot concatenate type 'Series' "
|
||||
"with object of type {type!r}"
|
||||
.format(type=type(x).__name__))
|
||||
if x.name is not None:
|
||||
names[i] = x.name
|
||||
has_names = True
|
||||
else:
|
||||
names[i] = num
|
||||
num += 1
|
||||
if has_names:
|
||||
return Index(names)
|
||||
else:
|
||||
return com._default_index(len(self.objs))
|
||||
else:
|
||||
return _ensure_index(self.keys)
|
||||
else:
|
||||
indexes = [x._data.axes[self.axis] for x in self.objs]
|
||||
|
||||
if self.ignore_index:
|
||||
idx = com._default_index(sum(len(i) for i in indexes))
|
||||
return idx
|
||||
|
||||
if self.keys is None:
|
||||
concat_axis = _concat_indexes(indexes)
|
||||
else:
|
||||
concat_axis = _make_concat_multiindex(indexes, self.keys,
|
||||
self.levels, self.names)
|
||||
|
||||
self._maybe_check_integrity(concat_axis)
|
||||
|
||||
return concat_axis
|
||||
|
||||
def _maybe_check_integrity(self, concat_index):
|
||||
if self.verify_integrity:
|
||||
if not concat_index.is_unique:
|
||||
overlap = concat_index[concat_index.duplicated()].unique()
|
||||
raise ValueError('Indexes have overlapping values: '
|
||||
'{overlap!s}'.format(overlap=overlap))
|
||||
|
||||
|
||||
def _concat_indexes(indexes):
|
||||
return indexes[0].append(indexes[1:])
|
||||
|
||||
|
||||
def _make_concat_multiindex(indexes, keys, levels=None, names=None):
|
||||
|
||||
if ((levels is None and isinstance(keys[0], tuple)) or
|
||||
(levels is not None and len(levels) > 1)):
|
||||
zipped = compat.lzip(*keys)
|
||||
if names is None:
|
||||
names = [None] * len(zipped)
|
||||
|
||||
if levels is None:
|
||||
_, levels = _factorize_from_iterables(zipped)
|
||||
else:
|
||||
levels = [_ensure_index(x) for x in levels]
|
||||
else:
|
||||
zipped = [keys]
|
||||
if names is None:
|
||||
names = [None]
|
||||
|
||||
if levels is None:
|
||||
levels = [_ensure_index(keys)]
|
||||
else:
|
||||
levels = [_ensure_index(x) for x in levels]
|
||||
|
||||
if not _all_indexes_same(indexes):
|
||||
label_list = []
|
||||
|
||||
# things are potentially different sizes, so compute the exact labels
|
||||
# for each level and pass those to MultiIndex.from_arrays
|
||||
|
||||
for hlevel, level in zip(zipped, levels):
|
||||
to_concat = []
|
||||
for key, index in zip(hlevel, indexes):
|
||||
try:
|
||||
i = level.get_loc(key)
|
||||
except KeyError:
|
||||
raise ValueError('Key {key!s} not in level {level!s}'
|
||||
.format(key=key, level=level))
|
||||
|
||||
to_concat.append(np.repeat(i, len(index)))
|
||||
label_list.append(np.concatenate(to_concat))
|
||||
|
||||
concat_index = _concat_indexes(indexes)
|
||||
|
||||
# these go at the end
|
||||
if isinstance(concat_index, MultiIndex):
|
||||
levels.extend(concat_index.levels)
|
||||
label_list.extend(concat_index.labels)
|
||||
else:
|
||||
codes, categories = _factorize_from_iterable(concat_index)
|
||||
levels.append(categories)
|
||||
label_list.append(codes)
|
||||
|
||||
if len(names) == len(levels):
|
||||
names = list(names)
|
||||
else:
|
||||
# make sure that all of the passed indices have the same nlevels
|
||||
if not len({idx.nlevels for idx in indexes}) == 1:
|
||||
raise AssertionError("Cannot concat indices that do"
|
||||
" not have the same number of levels")
|
||||
|
||||
# also copies
|
||||
names = names + _get_consensus_names(indexes)
|
||||
|
||||
return MultiIndex(levels=levels, labels=label_list, names=names,
|
||||
verify_integrity=False)
|
||||
|
||||
new_index = indexes[0]
|
||||
n = len(new_index)
|
||||
kpieces = len(indexes)
|
||||
|
||||
# also copies
|
||||
new_names = list(names)
|
||||
new_levels = list(levels)
|
||||
|
||||
# construct labels
|
||||
new_labels = []
|
||||
|
||||
# do something a bit more speedy
|
||||
|
||||
for hlevel, level in zip(zipped, levels):
|
||||
hlevel = _ensure_index(hlevel)
|
||||
mapped = level.get_indexer(hlevel)
|
||||
|
||||
mask = mapped == -1
|
||||
if mask.any():
|
||||
raise ValueError('Values not found in passed level: {hlevel!s}'
|
||||
.format(hlevel=hlevel[mask]))
|
||||
|
||||
new_labels.append(np.repeat(mapped, n))
|
||||
|
||||
if isinstance(new_index, MultiIndex):
|
||||
new_levels.extend(new_index.levels)
|
||||
new_labels.extend([np.tile(lab, kpieces) for lab in new_index.labels])
|
||||
else:
|
||||
new_levels.append(new_index)
|
||||
new_labels.append(np.tile(np.arange(n), kpieces))
|
||||
|
||||
if len(new_names) < len(new_levels):
|
||||
new_names.extend(new_index.names)
|
||||
|
||||
return MultiIndex(levels=new_levels, labels=new_labels, names=new_names,
|
||||
verify_integrity=False)
|
||||
@@ -0,0 +1,446 @@
|
||||
# pylint: disable=E1101,E1103
|
||||
# pylint: disable=W0703,W0622,W0613,W0201
|
||||
import numpy as np
|
||||
|
||||
from pandas.core.dtypes.common import is_list_like
|
||||
from pandas import compat
|
||||
from pandas.core.arrays import Categorical
|
||||
|
||||
from pandas.core.dtypes.generic import ABCMultiIndex
|
||||
|
||||
from pandas.core.frame import _shared_docs
|
||||
from pandas.util._decorators import Appender
|
||||
|
||||
import re
|
||||
from pandas.core.dtypes.missing import notna
|
||||
from pandas.core.dtypes.common import is_extension_type
|
||||
from pandas.core.tools.numeric import to_numeric
|
||||
from pandas.core.reshape.concat import concat
|
||||
|
||||
|
||||
@Appender(_shared_docs['melt'] %
|
||||
dict(caller='pd.melt(df, ',
|
||||
versionadded="",
|
||||
other='DataFrame.melt'))
|
||||
def melt(frame, id_vars=None, value_vars=None, var_name=None,
|
||||
value_name='value', col_level=None):
|
||||
# TODO: what about the existing index?
|
||||
if id_vars is not None:
|
||||
if not is_list_like(id_vars):
|
||||
id_vars = [id_vars]
|
||||
elif (isinstance(frame.columns, ABCMultiIndex) and
|
||||
not isinstance(id_vars, list)):
|
||||
raise ValueError('id_vars must be a list of tuples when columns'
|
||||
' are a MultiIndex')
|
||||
else:
|
||||
id_vars = list(id_vars)
|
||||
else:
|
||||
id_vars = []
|
||||
|
||||
if value_vars is not None:
|
||||
if not is_list_like(value_vars):
|
||||
value_vars = [value_vars]
|
||||
elif (isinstance(frame.columns, ABCMultiIndex) and
|
||||
not isinstance(value_vars, list)):
|
||||
raise ValueError('value_vars must be a list of tuples when'
|
||||
' columns are a MultiIndex')
|
||||
else:
|
||||
value_vars = list(value_vars)
|
||||
frame = frame.loc[:, id_vars + value_vars]
|
||||
else:
|
||||
frame = frame.copy()
|
||||
|
||||
if col_level is not None: # allow list or other?
|
||||
# frame is a copy
|
||||
frame.columns = frame.columns.get_level_values(col_level)
|
||||
|
||||
if var_name is None:
|
||||
if isinstance(frame.columns, ABCMultiIndex):
|
||||
if len(frame.columns.names) == len(set(frame.columns.names)):
|
||||
var_name = frame.columns.names
|
||||
else:
|
||||
var_name = ['variable_{i}'.format(i=i)
|
||||
for i in range(len(frame.columns.names))]
|
||||
else:
|
||||
var_name = [frame.columns.name if frame.columns.name is not None
|
||||
else 'variable']
|
||||
if isinstance(var_name, compat.string_types):
|
||||
var_name = [var_name]
|
||||
|
||||
N, K = frame.shape
|
||||
K -= len(id_vars)
|
||||
|
||||
mdata = {}
|
||||
for col in id_vars:
|
||||
id_data = frame.pop(col)
|
||||
if is_extension_type(id_data):
|
||||
id_data = concat([id_data] * K, ignore_index=True)
|
||||
else:
|
||||
id_data = np.tile(id_data.values, K)
|
||||
mdata[col] = id_data
|
||||
|
||||
mcolumns = id_vars + var_name + [value_name]
|
||||
|
||||
mdata[value_name] = frame.values.ravel('F')
|
||||
for i, col in enumerate(var_name):
|
||||
# asanyarray will keep the columns as an Index
|
||||
mdata[col] = np.asanyarray(frame.columns
|
||||
._get_level_values(i)).repeat(N)
|
||||
|
||||
return frame._constructor(mdata, columns=mcolumns)
|
||||
|
||||
|
||||
def lreshape(data, groups, dropna=True, label=None):
|
||||
"""
|
||||
Reshape long-format data to wide. Generalized inverse of DataFrame.pivot
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : DataFrame
|
||||
groups : dict
|
||||
{new_name : list_of_columns}
|
||||
dropna : boolean, default True
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import pandas as pd
|
||||
>>> data = pd.DataFrame({'hr1': [514, 573], 'hr2': [545, 526],
|
||||
... 'team': ['Red Sox', 'Yankees'],
|
||||
... 'year1': [2007, 2007], 'year2': [2008, 2008]})
|
||||
>>> data
|
||||
hr1 hr2 team year1 year2
|
||||
0 514 545 Red Sox 2007 2008
|
||||
1 573 526 Yankees 2007 2008
|
||||
|
||||
>>> pd.lreshape(data, {'year': ['year1', 'year2'], 'hr': ['hr1', 'hr2']})
|
||||
team year hr
|
||||
0 Red Sox 2007 514
|
||||
1 Yankees 2007 573
|
||||
2 Red Sox 2008 545
|
||||
3 Yankees 2008 526
|
||||
|
||||
Returns
|
||||
-------
|
||||
reshaped : DataFrame
|
||||
"""
|
||||
if isinstance(groups, dict):
|
||||
keys = list(groups.keys())
|
||||
values = list(groups.values())
|
||||
else:
|
||||
keys, values = zip(*groups)
|
||||
|
||||
all_cols = list(set.union(*[set(x) for x in values]))
|
||||
id_cols = list(data.columns.difference(all_cols))
|
||||
|
||||
K = len(values[0])
|
||||
|
||||
for seq in values:
|
||||
if len(seq) != K:
|
||||
raise ValueError('All column lists must be same length')
|
||||
|
||||
mdata = {}
|
||||
pivot_cols = []
|
||||
|
||||
for target, names in zip(keys, values):
|
||||
to_concat = [data[col].values for col in names]
|
||||
|
||||
import pandas.core.dtypes.concat as _concat
|
||||
mdata[target] = _concat._concat_compat(to_concat)
|
||||
pivot_cols.append(target)
|
||||
|
||||
for col in id_cols:
|
||||
mdata[col] = np.tile(data[col].values, K)
|
||||
|
||||
if dropna:
|
||||
mask = np.ones(len(mdata[pivot_cols[0]]), dtype=bool)
|
||||
for c in pivot_cols:
|
||||
mask &= notna(mdata[c])
|
||||
if not mask.all():
|
||||
mdata = {k: v[mask] for k, v in compat.iteritems(mdata)}
|
||||
|
||||
return data._constructor(mdata, columns=id_cols + pivot_cols)
|
||||
|
||||
|
||||
def wide_to_long(df, stubnames, i, j, sep="", suffix=r'\d+'):
|
||||
r"""
|
||||
Wide panel to long format. Less flexible but more user-friendly than melt.
|
||||
|
||||
With stubnames ['A', 'B'], this function expects to find one or more
|
||||
group of columns with format Asuffix1, Asuffix2,..., Bsuffix1, Bsuffix2,...
|
||||
You specify what you want to call this suffix in the resulting long format
|
||||
with `j` (for example `j='year'`)
|
||||
|
||||
Each row of these wide variables are assumed to be uniquely identified by
|
||||
`i` (can be a single column name or a list of column names)
|
||||
|
||||
All remaining variables in the data frame are left intact.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
df : DataFrame
|
||||
The wide-format DataFrame
|
||||
stubnames : str or list-like
|
||||
The stub name(s). The wide format variables are assumed to
|
||||
start with the stub names.
|
||||
i : str or list-like
|
||||
Column(s) to use as id variable(s)
|
||||
j : str
|
||||
The name of the subobservation variable. What you wish to name your
|
||||
suffix in the long format.
|
||||
sep : str, default ""
|
||||
A character indicating the separation of the variable names
|
||||
in the wide format, to be stripped from the names in the long format.
|
||||
For example, if your column names are A-suffix1, A-suffix2, you
|
||||
can strip the hyphen by specifying `sep='-'`
|
||||
|
||||
.. versionadded:: 0.20.0
|
||||
|
||||
suffix : str, default '\\d+'
|
||||
A regular expression capturing the wanted suffixes. '\\d+' captures
|
||||
numeric suffixes. Suffixes with no numbers could be specified with the
|
||||
negated character class '\\D+'. You can also further disambiguate
|
||||
suffixes, for example, if your wide variables are of the form
|
||||
Aone, Btwo,.., and you have an unrelated column Arating, you can
|
||||
ignore the last one by specifying `suffix='(!?one|two)'`
|
||||
|
||||
.. versionadded:: 0.20.0
|
||||
|
||||
.. versionchanged:: 0.23.0
|
||||
When all suffixes are numeric, they are cast to int64/float64.
|
||||
|
||||
Returns
|
||||
-------
|
||||
DataFrame
|
||||
A DataFrame that contains each stub name as a variable, with new index
|
||||
(i, j)
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import pandas as pd
|
||||
>>> import numpy as np
|
||||
>>> np.random.seed(123)
|
||||
>>> df = pd.DataFrame({"A1970" : {0 : "a", 1 : "b", 2 : "c"},
|
||||
... "A1980" : {0 : "d", 1 : "e", 2 : "f"},
|
||||
... "B1970" : {0 : 2.5, 1 : 1.2, 2 : .7},
|
||||
... "B1980" : {0 : 3.2, 1 : 1.3, 2 : .1},
|
||||
... "X" : dict(zip(range(3), np.random.randn(3)))
|
||||
... })
|
||||
>>> df["id"] = df.index
|
||||
>>> df
|
||||
A1970 A1980 B1970 B1980 X id
|
||||
0 a d 2.5 3.2 -1.085631 0
|
||||
1 b e 1.2 1.3 0.997345 1
|
||||
2 c f 0.7 0.1 0.282978 2
|
||||
>>> pd.wide_to_long(df, ["A", "B"], i="id", j="year")
|
||||
... # doctest: +NORMALIZE_WHITESPACE
|
||||
X A B
|
||||
id year
|
||||
0 1970 -1.085631 a 2.5
|
||||
1 1970 0.997345 b 1.2
|
||||
2 1970 0.282978 c 0.7
|
||||
0 1980 -1.085631 d 3.2
|
||||
1 1980 0.997345 e 1.3
|
||||
2 1980 0.282978 f 0.1
|
||||
|
||||
With multuple id columns
|
||||
|
||||
>>> df = pd.DataFrame({
|
||||
... 'famid': [1, 1, 1, 2, 2, 2, 3, 3, 3],
|
||||
... 'birth': [1, 2, 3, 1, 2, 3, 1, 2, 3],
|
||||
... 'ht1': [2.8, 2.9, 2.2, 2, 1.8, 1.9, 2.2, 2.3, 2.1],
|
||||
... 'ht2': [3.4, 3.8, 2.9, 3.2, 2.8, 2.4, 3.3, 3.4, 2.9]
|
||||
... })
|
||||
>>> df
|
||||
birth famid ht1 ht2
|
||||
0 1 1 2.8 3.4
|
||||
1 2 1 2.9 3.8
|
||||
2 3 1 2.2 2.9
|
||||
3 1 2 2.0 3.2
|
||||
4 2 2 1.8 2.8
|
||||
5 3 2 1.9 2.4
|
||||
6 1 3 2.2 3.3
|
||||
7 2 3 2.3 3.4
|
||||
8 3 3 2.1 2.9
|
||||
>>> l = pd.wide_to_long(df, stubnames='ht', i=['famid', 'birth'], j='age')
|
||||
>>> l
|
||||
... # doctest: +NORMALIZE_WHITESPACE
|
||||
ht
|
||||
famid birth age
|
||||
1 1 1 2.8
|
||||
2 3.4
|
||||
2 1 2.9
|
||||
2 3.8
|
||||
3 1 2.2
|
||||
2 2.9
|
||||
2 1 1 2.0
|
||||
2 3.2
|
||||
2 1 1.8
|
||||
2 2.8
|
||||
3 1 1.9
|
||||
2 2.4
|
||||
3 1 1 2.2
|
||||
2 3.3
|
||||
2 1 2.3
|
||||
2 3.4
|
||||
3 1 2.1
|
||||
2 2.9
|
||||
|
||||
Going from long back to wide just takes some creative use of `unstack`
|
||||
|
||||
>>> w = l.unstack()
|
||||
>>> w.columns = w.columns.map('{0[0]}{0[1]}'.format)
|
||||
>>> w.reset_index()
|
||||
famid birth ht1 ht2
|
||||
0 1 1 2.8 3.4
|
||||
1 1 2 2.9 3.8
|
||||
2 1 3 2.2 2.9
|
||||
3 2 1 2.0 3.2
|
||||
4 2 2 1.8 2.8
|
||||
5 2 3 1.9 2.4
|
||||
6 3 1 2.2 3.3
|
||||
7 3 2 2.3 3.4
|
||||
8 3 3 2.1 2.9
|
||||
|
||||
Less wieldy column names are also handled
|
||||
|
||||
>>> np.random.seed(0)
|
||||
>>> df = pd.DataFrame({'A(quarterly)-2010': np.random.rand(3),
|
||||
... 'A(quarterly)-2011': np.random.rand(3),
|
||||
... 'B(quarterly)-2010': np.random.rand(3),
|
||||
... 'B(quarterly)-2011': np.random.rand(3),
|
||||
... 'X' : np.random.randint(3, size=3)})
|
||||
>>> df['id'] = df.index
|
||||
>>> df # doctest: +NORMALIZE_WHITESPACE, +ELLIPSIS
|
||||
A(quarterly)-2010 A(quarterly)-2011 B(quarterly)-2010 ...
|
||||
0 0.548814 0.544883 0.437587 ...
|
||||
1 0.715189 0.423655 0.891773 ...
|
||||
2 0.602763 0.645894 0.963663 ...
|
||||
X id
|
||||
0 0 0
|
||||
1 1 1
|
||||
2 1 2
|
||||
|
||||
>>> pd.wide_to_long(df, ['A(quarterly)', 'B(quarterly)'], i='id',
|
||||
... j='year', sep='-')
|
||||
... # doctest: +NORMALIZE_WHITESPACE
|
||||
X A(quarterly) B(quarterly)
|
||||
id year
|
||||
0 2010 0 0.548814 0.437587
|
||||
1 2010 1 0.715189 0.891773
|
||||
2 2010 1 0.602763 0.963663
|
||||
0 2011 0 0.544883 0.383442
|
||||
1 2011 1 0.423655 0.791725
|
||||
2 2011 1 0.645894 0.528895
|
||||
|
||||
If we have many columns, we could also use a regex to find our
|
||||
stubnames and pass that list on to wide_to_long
|
||||
|
||||
>>> stubnames = sorted(
|
||||
... set([match[0] for match in df.columns.str.findall(
|
||||
... r'[A-B]\(.*\)').values if match != [] ])
|
||||
... )
|
||||
>>> list(stubnames)
|
||||
['A(quarterly)', 'B(quarterly)']
|
||||
|
||||
All of the above examples have integers as suffixes. It is possible to
|
||||
have non-integers as suffixes.
|
||||
|
||||
>>> df = pd.DataFrame({
|
||||
... 'famid': [1, 1, 1, 2, 2, 2, 3, 3, 3],
|
||||
... 'birth': [1, 2, 3, 1, 2, 3, 1, 2, 3],
|
||||
... 'ht_one': [2.8, 2.9, 2.2, 2, 1.8, 1.9, 2.2, 2.3, 2.1],
|
||||
... 'ht_two': [3.4, 3.8, 2.9, 3.2, 2.8, 2.4, 3.3, 3.4, 2.9]
|
||||
... })
|
||||
>>> df
|
||||
birth famid ht_one ht_two
|
||||
0 1 1 2.8 3.4
|
||||
1 2 1 2.9 3.8
|
||||
2 3 1 2.2 2.9
|
||||
3 1 2 2.0 3.2
|
||||
4 2 2 1.8 2.8
|
||||
5 3 2 1.9 2.4
|
||||
6 1 3 2.2 3.3
|
||||
7 2 3 2.3 3.4
|
||||
8 3 3 2.1 2.9
|
||||
|
||||
>>> l = pd.wide_to_long(df, stubnames='ht', i=['famid', 'birth'], j='age',
|
||||
sep='_', suffix='\w')
|
||||
>>> l
|
||||
... # doctest: +NORMALIZE_WHITESPACE
|
||||
ht
|
||||
famid birth age
|
||||
1 1 one 2.8
|
||||
two 3.4
|
||||
2 one 2.9
|
||||
two 3.8
|
||||
3 one 2.2
|
||||
two 2.9
|
||||
2 1 one 2.0
|
||||
two 3.2
|
||||
2 one 1.8
|
||||
two 2.8
|
||||
3 one 1.9
|
||||
two 2.4
|
||||
3 1 one 2.2
|
||||
two 3.3
|
||||
2 one 2.3
|
||||
two 3.4
|
||||
3 one 2.1
|
||||
two 2.9
|
||||
|
||||
Notes
|
||||
-----
|
||||
All extra variables are left untouched. This simply uses
|
||||
`pandas.melt` under the hood, but is hard-coded to "do the right thing"
|
||||
in a typical case.
|
||||
"""
|
||||
def get_var_names(df, stub, sep, suffix):
|
||||
regex = r'^{stub}{sep}{suffix}$'.format(
|
||||
stub=re.escape(stub), sep=re.escape(sep), suffix=suffix)
|
||||
pattern = re.compile(regex)
|
||||
return [col for col in df.columns if pattern.match(col)]
|
||||
|
||||
def melt_stub(df, stub, i, j, value_vars, sep):
|
||||
newdf = melt(df, id_vars=i, value_vars=value_vars,
|
||||
value_name=stub.rstrip(sep), var_name=j)
|
||||
newdf[j] = Categorical(newdf[j])
|
||||
newdf[j] = newdf[j].str.replace(re.escape(stub + sep), "")
|
||||
|
||||
# GH17627 Cast numerics suffixes to int/float
|
||||
newdf[j] = to_numeric(newdf[j], errors='ignore')
|
||||
|
||||
return newdf.set_index(i + [j])
|
||||
|
||||
if any(col in stubnames for col in df.columns):
|
||||
raise ValueError("stubname can't be identical to a column name")
|
||||
|
||||
if not is_list_like(stubnames):
|
||||
stubnames = [stubnames]
|
||||
else:
|
||||
stubnames = list(stubnames)
|
||||
|
||||
if not is_list_like(i):
|
||||
i = [i]
|
||||
else:
|
||||
i = list(i)
|
||||
|
||||
if df[i].duplicated().any():
|
||||
raise ValueError("the id variables need to uniquely identify each row")
|
||||
|
||||
value_vars = [get_var_names(df, stub, sep, suffix) for stub in stubnames]
|
||||
|
||||
value_vars_flattened = [e for sublist in value_vars for e in sublist]
|
||||
id_vars = list(set(df.columns.tolist()).difference(value_vars_flattened))
|
||||
|
||||
melted = []
|
||||
for s, v in zip(stubnames, value_vars):
|
||||
melted.append(melt_stub(df, s, i, j, v, sep))
|
||||
melted = melted[0].join(melted[1:], how='outer')
|
||||
|
||||
if len(i) == 1:
|
||||
new = df[id_vars].set_index(i).join(melted)
|
||||
return new
|
||||
|
||||
new = df[id_vars].merge(melted.reset_index(), on=i).set_index(i + [j])
|
||||
|
||||
return new
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,589 @@
|
||||
# pylint: disable=E1103
|
||||
|
||||
|
||||
from pandas.core.dtypes.common import (
|
||||
is_list_like, is_scalar, is_integer_dtype)
|
||||
from pandas.core.dtypes.generic import ABCDataFrame, ABCSeries
|
||||
from pandas.core.dtypes.cast import maybe_downcast_to_dtype
|
||||
|
||||
from pandas.core.reshape.concat import concat
|
||||
from pandas.core.series import Series
|
||||
from pandas.core.groupby.groupby import Grouper
|
||||
from pandas.core.reshape.util import cartesian_product
|
||||
from pandas.core.index import Index, _get_objs_combined_axis
|
||||
from pandas.compat import range, lrange, zip
|
||||
from pandas import compat
|
||||
import pandas.core.common as com
|
||||
from pandas.util._decorators import Appender, Substitution
|
||||
|
||||
from pandas.core.frame import _shared_docs
|
||||
# Note: We need to make sure `frame` is imported before `pivot`, otherwise
|
||||
# _shared_docs['pivot_table'] will not yet exist. TODO: Fix this dependency
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
@Substitution('\ndata : DataFrame')
|
||||
@Appender(_shared_docs['pivot_table'], indents=1)
|
||||
def pivot_table(data, values=None, index=None, columns=None, aggfunc='mean',
|
||||
fill_value=None, margins=False, dropna=True,
|
||||
margins_name='All'):
|
||||
index = _convert_by(index)
|
||||
columns = _convert_by(columns)
|
||||
|
||||
if isinstance(aggfunc, list):
|
||||
pieces = []
|
||||
keys = []
|
||||
for func in aggfunc:
|
||||
table = pivot_table(data, values=values, index=index,
|
||||
columns=columns,
|
||||
fill_value=fill_value, aggfunc=func,
|
||||
margins=margins, margins_name=margins_name)
|
||||
pieces.append(table)
|
||||
keys.append(getattr(func, '__name__', func))
|
||||
|
||||
return concat(pieces, keys=keys, axis=1)
|
||||
|
||||
keys = index + columns
|
||||
|
||||
values_passed = values is not None
|
||||
if values_passed:
|
||||
if is_list_like(values):
|
||||
values_multi = True
|
||||
values = list(values)
|
||||
else:
|
||||
values_multi = False
|
||||
values = [values]
|
||||
|
||||
# GH14938 Make sure value labels are in data
|
||||
for i in values:
|
||||
if i not in data:
|
||||
raise KeyError(i)
|
||||
|
||||
to_filter = []
|
||||
for x in keys + values:
|
||||
if isinstance(x, Grouper):
|
||||
x = x.key
|
||||
try:
|
||||
if x in data:
|
||||
to_filter.append(x)
|
||||
except TypeError:
|
||||
pass
|
||||
if len(to_filter) < len(data.columns):
|
||||
data = data[to_filter]
|
||||
|
||||
else:
|
||||
values = data.columns
|
||||
for key in keys:
|
||||
try:
|
||||
values = values.drop(key)
|
||||
except (TypeError, ValueError, KeyError):
|
||||
pass
|
||||
values = list(values)
|
||||
|
||||
# group by the cartesian product of the grouper
|
||||
# if we have a categorical
|
||||
grouped = data.groupby(keys, observed=False)
|
||||
agged = grouped.agg(aggfunc)
|
||||
if dropna and isinstance(agged, ABCDataFrame) and len(agged.columns):
|
||||
agged = agged.dropna(how='all')
|
||||
|
||||
# gh-21133
|
||||
# we want to down cast if
|
||||
# the original values are ints
|
||||
# as we grouped with a NaN value
|
||||
# and then dropped, coercing to floats
|
||||
for v in [v for v in values if v in data and v in agged]:
|
||||
if (is_integer_dtype(data[v]) and
|
||||
not is_integer_dtype(agged[v])):
|
||||
agged[v] = maybe_downcast_to_dtype(agged[v], data[v].dtype)
|
||||
|
||||
table = agged
|
||||
if table.index.nlevels > 1:
|
||||
# Related GH #17123
|
||||
# If index_names are integers, determine whether the integers refer
|
||||
# to the level position or name.
|
||||
index_names = agged.index.names[:len(index)]
|
||||
to_unstack = []
|
||||
for i in range(len(index), len(keys)):
|
||||
name = agged.index.names[i]
|
||||
if name is None or name in index_names:
|
||||
to_unstack.append(i)
|
||||
else:
|
||||
to_unstack.append(name)
|
||||
table = agged.unstack(to_unstack)
|
||||
|
||||
if not dropna:
|
||||
from pandas import MultiIndex
|
||||
if table.index.nlevels > 1:
|
||||
m = MultiIndex.from_arrays(cartesian_product(table.index.levels),
|
||||
names=table.index.names)
|
||||
table = table.reindex(m, axis=0)
|
||||
|
||||
if table.columns.nlevels > 1:
|
||||
m = MultiIndex.from_arrays(cartesian_product(table.columns.levels),
|
||||
names=table.columns.names)
|
||||
table = table.reindex(m, axis=1)
|
||||
|
||||
if isinstance(table, ABCDataFrame):
|
||||
table = table.sort_index(axis=1)
|
||||
|
||||
if fill_value is not None:
|
||||
table = table.fillna(value=fill_value, downcast='infer')
|
||||
|
||||
if margins:
|
||||
if dropna:
|
||||
data = data[data.notna().all(axis=1)]
|
||||
table = _add_margins(table, data, values, rows=index,
|
||||
cols=columns, aggfunc=aggfunc,
|
||||
observed=dropna,
|
||||
margins_name=margins_name, fill_value=fill_value)
|
||||
|
||||
# discard the top level
|
||||
if values_passed and not values_multi and not table.empty and \
|
||||
(table.columns.nlevels > 1):
|
||||
table = table[values[0]]
|
||||
|
||||
if len(index) == 0 and len(columns) > 0:
|
||||
table = table.T
|
||||
|
||||
# GH 15193 Make sure empty columns are removed if dropna=True
|
||||
if isinstance(table, ABCDataFrame) and dropna:
|
||||
table = table.dropna(how='all', axis=1)
|
||||
|
||||
return table
|
||||
|
||||
|
||||
def _add_margins(table, data, values, rows, cols, aggfunc,
|
||||
observed=None, margins_name='All', fill_value=None):
|
||||
if not isinstance(margins_name, compat.string_types):
|
||||
raise ValueError('margins_name argument must be a string')
|
||||
|
||||
msg = u'Conflicting name "{name}" in margins'.format(name=margins_name)
|
||||
for level in table.index.names:
|
||||
if margins_name in table.index.get_level_values(level):
|
||||
raise ValueError(msg)
|
||||
|
||||
grand_margin = _compute_grand_margin(data, values, aggfunc, margins_name)
|
||||
|
||||
# could be passed a Series object with no 'columns'
|
||||
if hasattr(table, 'columns'):
|
||||
for level in table.columns.names[1:]:
|
||||
if margins_name in table.columns.get_level_values(level):
|
||||
raise ValueError(msg)
|
||||
|
||||
if len(rows) > 1:
|
||||
key = (margins_name,) + ('',) * (len(rows) - 1)
|
||||
else:
|
||||
key = margins_name
|
||||
|
||||
if not values and isinstance(table, ABCSeries):
|
||||
# If there are no values and the table is a series, then there is only
|
||||
# one column in the data. Compute grand margin and return it.
|
||||
return table.append(Series({key: grand_margin[margins_name]}))
|
||||
|
||||
if values:
|
||||
marginal_result_set = _generate_marginal_results(table, data, values,
|
||||
rows, cols, aggfunc,
|
||||
observed,
|
||||
grand_margin,
|
||||
margins_name)
|
||||
if not isinstance(marginal_result_set, tuple):
|
||||
return marginal_result_set
|
||||
result, margin_keys, row_margin = marginal_result_set
|
||||
else:
|
||||
marginal_result_set = _generate_marginal_results_without_values(
|
||||
table, data, rows, cols, aggfunc, observed, margins_name)
|
||||
if not isinstance(marginal_result_set, tuple):
|
||||
return marginal_result_set
|
||||
result, margin_keys, row_margin = marginal_result_set
|
||||
row_margin = row_margin.reindex(result.columns, fill_value=fill_value)
|
||||
# populate grand margin
|
||||
for k in margin_keys:
|
||||
if isinstance(k, compat.string_types):
|
||||
row_margin[k] = grand_margin[k]
|
||||
else:
|
||||
row_margin[k] = grand_margin[k[0]]
|
||||
|
||||
from pandas import DataFrame
|
||||
margin_dummy = DataFrame(row_margin, columns=[key]).T
|
||||
|
||||
row_names = result.index.names
|
||||
try:
|
||||
for dtype in set(result.dtypes):
|
||||
cols = result.select_dtypes([dtype]).columns
|
||||
margin_dummy[cols] = margin_dummy[cols].astype(dtype)
|
||||
result = result.append(margin_dummy)
|
||||
except TypeError:
|
||||
|
||||
# we cannot reshape, so coerce the axis
|
||||
result.index = result.index._to_safe_for_reshape()
|
||||
result = result.append(margin_dummy)
|
||||
result.index.names = row_names
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _compute_grand_margin(data, values, aggfunc,
|
||||
margins_name='All'):
|
||||
|
||||
if values:
|
||||
grand_margin = {}
|
||||
for k, v in data[values].iteritems():
|
||||
try:
|
||||
if isinstance(aggfunc, compat.string_types):
|
||||
grand_margin[k] = getattr(v, aggfunc)()
|
||||
elif isinstance(aggfunc, dict):
|
||||
if isinstance(aggfunc[k], compat.string_types):
|
||||
grand_margin[k] = getattr(v, aggfunc[k])()
|
||||
else:
|
||||
grand_margin[k] = aggfunc[k](v)
|
||||
else:
|
||||
grand_margin[k] = aggfunc(v)
|
||||
except TypeError:
|
||||
pass
|
||||
return grand_margin
|
||||
else:
|
||||
return {margins_name: aggfunc(data.index)}
|
||||
|
||||
|
||||
def _generate_marginal_results(table, data, values, rows, cols, aggfunc,
|
||||
observed,
|
||||
grand_margin,
|
||||
margins_name='All'):
|
||||
if len(cols) > 0:
|
||||
# need to "interleave" the margins
|
||||
table_pieces = []
|
||||
margin_keys = []
|
||||
|
||||
def _all_key(key):
|
||||
return (key, margins_name) + ('',) * (len(cols) - 1)
|
||||
|
||||
if len(rows) > 0:
|
||||
margin = data[rows + values].groupby(
|
||||
rows, observed=observed).agg(aggfunc)
|
||||
cat_axis = 1
|
||||
|
||||
for key, piece in table.groupby(level=0,
|
||||
axis=cat_axis,
|
||||
observed=observed):
|
||||
all_key = _all_key(key)
|
||||
|
||||
# we are going to mutate this, so need to copy!
|
||||
piece = piece.copy()
|
||||
try:
|
||||
piece[all_key] = margin[key]
|
||||
except TypeError:
|
||||
|
||||
# we cannot reshape, so coerce the axis
|
||||
piece.set_axis(piece._get_axis(
|
||||
cat_axis)._to_safe_for_reshape(),
|
||||
axis=cat_axis, inplace=True)
|
||||
piece[all_key] = margin[key]
|
||||
|
||||
table_pieces.append(piece)
|
||||
margin_keys.append(all_key)
|
||||
else:
|
||||
margin = grand_margin
|
||||
cat_axis = 0
|
||||
for key, piece in table.groupby(level=0,
|
||||
axis=cat_axis,
|
||||
observed=observed):
|
||||
all_key = _all_key(key)
|
||||
table_pieces.append(piece)
|
||||
table_pieces.append(Series(margin[key], index=[all_key]))
|
||||
margin_keys.append(all_key)
|
||||
|
||||
result = concat(table_pieces, axis=cat_axis)
|
||||
|
||||
if len(rows) == 0:
|
||||
return result
|
||||
else:
|
||||
result = table
|
||||
margin_keys = table.columns
|
||||
|
||||
if len(cols) > 0:
|
||||
row_margin = data[cols + values].groupby(
|
||||
cols, observed=observed).agg(aggfunc)
|
||||
row_margin = row_margin.stack()
|
||||
|
||||
# slight hack
|
||||
new_order = [len(cols)] + lrange(len(cols))
|
||||
row_margin.index = row_margin.index.reorder_levels(new_order)
|
||||
else:
|
||||
row_margin = Series(np.nan, index=result.columns)
|
||||
|
||||
return result, margin_keys, row_margin
|
||||
|
||||
|
||||
def _generate_marginal_results_without_values(
|
||||
table, data, rows, cols, aggfunc,
|
||||
observed, margins_name='All'):
|
||||
if len(cols) > 0:
|
||||
# need to "interleave" the margins
|
||||
margin_keys = []
|
||||
|
||||
def _all_key():
|
||||
if len(cols) == 1:
|
||||
return margins_name
|
||||
return (margins_name, ) + ('', ) * (len(cols) - 1)
|
||||
|
||||
if len(rows) > 0:
|
||||
margin = data[rows].groupby(rows,
|
||||
observed=observed).apply(aggfunc)
|
||||
all_key = _all_key()
|
||||
table[all_key] = margin
|
||||
result = table
|
||||
margin_keys.append(all_key)
|
||||
|
||||
else:
|
||||
margin = data.groupby(level=0,
|
||||
axis=0,
|
||||
observed=observed).apply(aggfunc)
|
||||
all_key = _all_key()
|
||||
table[all_key] = margin
|
||||
result = table
|
||||
margin_keys.append(all_key)
|
||||
return result
|
||||
else:
|
||||
result = table
|
||||
margin_keys = table.columns
|
||||
|
||||
if len(cols):
|
||||
row_margin = data[cols].groupby(cols, observed=observed).apply(aggfunc)
|
||||
else:
|
||||
row_margin = Series(np.nan, index=result.columns)
|
||||
|
||||
return result, margin_keys, row_margin
|
||||
|
||||
|
||||
def _convert_by(by):
|
||||
if by is None:
|
||||
by = []
|
||||
elif (is_scalar(by) or
|
||||
isinstance(by, (np.ndarray, Index, ABCSeries, Grouper)) or
|
||||
hasattr(by, '__call__')):
|
||||
by = [by]
|
||||
else:
|
||||
by = list(by)
|
||||
return by
|
||||
|
||||
|
||||
def crosstab(index, columns, values=None, rownames=None, colnames=None,
|
||||
aggfunc=None, margins=False, margins_name='All', dropna=True,
|
||||
normalize=False):
|
||||
"""
|
||||
Compute a simple cross-tabulation of two (or more) factors. By default
|
||||
computes a frequency table of the factors unless an array of values and an
|
||||
aggregation function are passed
|
||||
|
||||
Parameters
|
||||
----------
|
||||
index : array-like, Series, or list of arrays/Series
|
||||
Values to group by in the rows
|
||||
columns : array-like, Series, or list of arrays/Series
|
||||
Values to group by in the columns
|
||||
values : array-like, optional
|
||||
Array of values to aggregate according to the factors.
|
||||
Requires `aggfunc` be specified.
|
||||
aggfunc : function, optional
|
||||
If specified, requires `values` be specified as well
|
||||
rownames : sequence, default None
|
||||
If passed, must match number of row arrays passed
|
||||
colnames : sequence, default None
|
||||
If passed, must match number of column arrays passed
|
||||
margins : boolean, default False
|
||||
Add row/column margins (subtotals)
|
||||
margins_name : string, default 'All'
|
||||
Name of the row / column that will contain the totals
|
||||
when margins is True.
|
||||
|
||||
.. versionadded:: 0.21.0
|
||||
|
||||
dropna : boolean, default True
|
||||
Do not include columns whose entries are all NaN
|
||||
normalize : boolean, {'all', 'index', 'columns'}, or {0,1}, default False
|
||||
Normalize by dividing all values by the sum of values.
|
||||
|
||||
- If passed 'all' or `True`, will normalize over all values.
|
||||
- If passed 'index' will normalize over each row.
|
||||
- If passed 'columns' will normalize over each column.
|
||||
- If margins is `True`, will also normalize margin values.
|
||||
|
||||
.. versionadded:: 0.18.1
|
||||
|
||||
|
||||
Notes
|
||||
-----
|
||||
Any Series passed will have their name attributes used unless row or column
|
||||
names for the cross-tabulation are specified.
|
||||
|
||||
Any input passed containing Categorical data will have **all** of its
|
||||
categories included in the cross-tabulation, even if the actual data does
|
||||
not contain any instances of a particular category.
|
||||
|
||||
In the event that there aren't overlapping indexes an empty DataFrame will
|
||||
be returned.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> a = np.array(["foo", "foo", "foo", "foo", "bar", "bar",
|
||||
... "bar", "bar", "foo", "foo", "foo"], dtype=object)
|
||||
>>> b = np.array(["one", "one", "one", "two", "one", "one",
|
||||
... "one", "two", "two", "two", "one"], dtype=object)
|
||||
>>> c = np.array(["dull", "dull", "shiny", "dull", "dull", "shiny",
|
||||
... "shiny", "dull", "shiny", "shiny", "shiny"],
|
||||
... dtype=object)
|
||||
|
||||
>>> pd.crosstab(a, [b, c], rownames=['a'], colnames=['b', 'c'])
|
||||
... # doctest: +NORMALIZE_WHITESPACE
|
||||
b one two
|
||||
c dull shiny dull shiny
|
||||
a
|
||||
bar 1 2 1 0
|
||||
foo 2 2 1 2
|
||||
|
||||
>>> foo = pd.Categorical(['a', 'b'], categories=['a', 'b', 'c'])
|
||||
>>> bar = pd.Categorical(['d', 'e'], categories=['d', 'e', 'f'])
|
||||
>>> crosstab(foo, bar) # 'c' and 'f' are not represented in the data,
|
||||
... # but they still will be counted in the output
|
||||
... # doctest: +SKIP
|
||||
col_0 d e f
|
||||
row_0
|
||||
a 1 0 0
|
||||
b 0 1 0
|
||||
c 0 0 0
|
||||
|
||||
Returns
|
||||
-------
|
||||
crosstab : DataFrame
|
||||
"""
|
||||
|
||||
index = com._maybe_make_list(index)
|
||||
columns = com._maybe_make_list(columns)
|
||||
|
||||
rownames = _get_names(index, rownames, prefix='row')
|
||||
colnames = _get_names(columns, colnames, prefix='col')
|
||||
|
||||
common_idx = _get_objs_combined_axis(index + columns, intersect=True,
|
||||
sort=False)
|
||||
|
||||
data = {}
|
||||
data.update(zip(rownames, index))
|
||||
data.update(zip(colnames, columns))
|
||||
|
||||
if values is None and aggfunc is not None:
|
||||
raise ValueError("aggfunc cannot be used without values.")
|
||||
|
||||
if values is not None and aggfunc is None:
|
||||
raise ValueError("values cannot be used without an aggfunc.")
|
||||
|
||||
from pandas import DataFrame
|
||||
df = DataFrame(data, index=common_idx)
|
||||
if values is None:
|
||||
df['__dummy__'] = 0
|
||||
kwargs = {'aggfunc': len, 'fill_value': 0}
|
||||
else:
|
||||
df['__dummy__'] = values
|
||||
kwargs = {'aggfunc': aggfunc}
|
||||
|
||||
table = df.pivot_table('__dummy__', index=rownames, columns=colnames,
|
||||
margins=margins, margins_name=margins_name,
|
||||
dropna=dropna, **kwargs)
|
||||
|
||||
# Post-process
|
||||
if normalize is not False:
|
||||
table = _normalize(table, normalize=normalize, margins=margins,
|
||||
margins_name=margins_name)
|
||||
|
||||
return table
|
||||
|
||||
|
||||
def _normalize(table, normalize, margins, margins_name='All'):
|
||||
|
||||
if not isinstance(normalize, bool) and not isinstance(normalize,
|
||||
compat.string_types):
|
||||
axis_subs = {0: 'index', 1: 'columns'}
|
||||
try:
|
||||
normalize = axis_subs[normalize]
|
||||
except KeyError:
|
||||
raise ValueError("Not a valid normalize argument")
|
||||
|
||||
if margins is False:
|
||||
|
||||
# Actual Normalizations
|
||||
normalizers = {
|
||||
'all': lambda x: x / x.sum(axis=1).sum(axis=0),
|
||||
'columns': lambda x: x / x.sum(),
|
||||
'index': lambda x: x.div(x.sum(axis=1), axis=0)
|
||||
}
|
||||
|
||||
normalizers[True] = normalizers['all']
|
||||
|
||||
try:
|
||||
f = normalizers[normalize]
|
||||
except KeyError:
|
||||
raise ValueError("Not a valid normalize argument")
|
||||
|
||||
table = f(table)
|
||||
table = table.fillna(0)
|
||||
|
||||
elif margins is True:
|
||||
|
||||
column_margin = table.loc[:, margins_name].drop(margins_name)
|
||||
index_margin = table.loc[margins_name, :].drop(margins_name)
|
||||
table = table.drop(margins_name, axis=1).drop(margins_name)
|
||||
# to keep index and columns names
|
||||
table_index_names = table.index.names
|
||||
table_columns_names = table.columns.names
|
||||
|
||||
# Normalize core
|
||||
table = _normalize(table, normalize=normalize, margins=False)
|
||||
|
||||
# Fix Margins
|
||||
if normalize == 'columns':
|
||||
column_margin = column_margin / column_margin.sum()
|
||||
table = concat([table, column_margin], axis=1)
|
||||
table = table.fillna(0)
|
||||
|
||||
elif normalize == 'index':
|
||||
index_margin = index_margin / index_margin.sum()
|
||||
table = table.append(index_margin)
|
||||
table = table.fillna(0)
|
||||
|
||||
elif normalize == "all" or normalize is True:
|
||||
column_margin = column_margin / column_margin.sum()
|
||||
index_margin = index_margin / index_margin.sum()
|
||||
index_margin.loc[margins_name] = 1
|
||||
table = concat([table, column_margin], axis=1)
|
||||
table = table.append(index_margin)
|
||||
|
||||
table = table.fillna(0)
|
||||
|
||||
else:
|
||||
raise ValueError("Not a valid normalize argument")
|
||||
|
||||
table.index.names = table_index_names
|
||||
table.columns.names = table_columns_names
|
||||
|
||||
else:
|
||||
raise ValueError("Not a valid margins argument")
|
||||
|
||||
return table
|
||||
|
||||
|
||||
def _get_names(arrs, names, prefix='row'):
|
||||
if names is None:
|
||||
names = []
|
||||
for i, arr in enumerate(arrs):
|
||||
if isinstance(arr, ABCSeries) and arr.name is not None:
|
||||
names.append(arr.name)
|
||||
else:
|
||||
names.append('{prefix}_{i}'.format(prefix=prefix, i=i))
|
||||
else:
|
||||
if len(names) != len(arrs):
|
||||
raise AssertionError('arrays and names must have the same length')
|
||||
if not isinstance(names, list):
|
||||
names = list(names)
|
||||
|
||||
return names
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,528 @@
|
||||
"""
|
||||
Quantilization functions and related stuff
|
||||
"""
|
||||
from functools import partial
|
||||
|
||||
from pandas.core.dtypes.missing import isna
|
||||
from pandas.core.dtypes.common import (
|
||||
is_integer,
|
||||
is_scalar,
|
||||
is_categorical_dtype,
|
||||
is_datetime64_dtype,
|
||||
is_timedelta64_dtype,
|
||||
is_datetime64tz_dtype,
|
||||
_ensure_int64)
|
||||
|
||||
import pandas.core.algorithms as algos
|
||||
import pandas.core.nanops as nanops
|
||||
from pandas._libs.lib import infer_dtype
|
||||
from pandas import (to_timedelta, to_datetime,
|
||||
Categorical, Timestamp, Timedelta,
|
||||
Series, Interval, IntervalIndex)
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def cut(x, bins, right=True, labels=None, retbins=False, precision=3,
|
||||
include_lowest=False, duplicates='raise'):
|
||||
"""
|
||||
Bin values into discrete intervals.
|
||||
|
||||
Use `cut` when you need to segment and sort data values into bins. This
|
||||
function is also useful for going from a continuous variable to a
|
||||
categorical variable. For example, `cut` could convert ages to groups of
|
||||
age ranges. Supports binning into an equal number of bins, or a
|
||||
pre-specified array of bins.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : array-like
|
||||
The input array to be binned. Must be 1-dimensional.
|
||||
bins : int, sequence of scalars, or pandas.IntervalIndex
|
||||
The criteria to bin by.
|
||||
|
||||
* int : Defines the number of equal-width bins in the range of `x`. The
|
||||
range of `x` is extended by .1% on each side to include the minimum
|
||||
and maximum values of `x`.
|
||||
* sequence of scalars : Defines the bin edges allowing for non-uniform
|
||||
width. No extension of the range of `x` is done.
|
||||
* IntervalIndex : Defines the exact bins to be used.
|
||||
|
||||
right : bool, default True
|
||||
Indicates whether `bins` includes the rightmost edge or not. If
|
||||
``right == True`` (the default), then the `bins` ``[1, 2, 3, 4]``
|
||||
indicate (1,2], (2,3], (3,4]. This argument is ignored when
|
||||
`bins` is an IntervalIndex.
|
||||
labels : array or bool, optional
|
||||
Specifies the labels for the returned bins. Must be the same length as
|
||||
the resulting bins. If False, returns only integer indicators of the
|
||||
bins. This affects the type of the output container (see below).
|
||||
This argument is ignored when `bins` is an IntervalIndex.
|
||||
retbins : bool, default False
|
||||
Whether to return the bins or not. Useful when bins is provided
|
||||
as a scalar.
|
||||
precision : int, default 3
|
||||
The precision at which to store and display the bins labels.
|
||||
include_lowest : bool, default False
|
||||
Whether the first interval should be left-inclusive or not.
|
||||
duplicates : {default 'raise', 'drop'}, optional
|
||||
If bin edges are not unique, raise ValueError or drop non-uniques.
|
||||
|
||||
.. versionadded:: 0.23.0
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : pandas.Categorical, Series, or ndarray
|
||||
An array-like object representing the respective bin for each value
|
||||
of `x`. The type depends on the value of `labels`.
|
||||
|
||||
* True (default) : returns a Series for Series `x` or a
|
||||
pandas.Categorical for all other inputs. The values stored within
|
||||
are Interval dtype.
|
||||
|
||||
* sequence of scalars : returns a Series for Series `x` or a
|
||||
pandas.Categorical for all other inputs. The values stored within
|
||||
are whatever the type in the sequence is.
|
||||
|
||||
* False : returns an ndarray of integers.
|
||||
|
||||
bins : numpy.ndarray or IntervalIndex.
|
||||
The computed or specified bins. Only returned when `retbins=True`.
|
||||
For scalar or sequence `bins`, this is an ndarray with the computed
|
||||
bins. If set `duplicates=drop`, `bins` will drop non-unique bin. For
|
||||
an IntervalIndex `bins`, this is equal to `bins`.
|
||||
|
||||
See Also
|
||||
--------
|
||||
qcut : Discretize variable into equal-sized buckets based on rank
|
||||
or based on sample quantiles.
|
||||
pandas.Categorical : Array type for storing data that come from a
|
||||
fixed set of values.
|
||||
Series : One-dimensional array with axis labels (including time series).
|
||||
pandas.IntervalIndex : Immutable Index implementing an ordered,
|
||||
sliceable set.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Any NA values will be NA in the result. Out of bounds values will be NA in
|
||||
the resulting Series or pandas.Categorical object.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Discretize into three equal-sized bins.
|
||||
|
||||
>>> pd.cut(np.array([1, 7, 5, 4, 6, 3]), 3)
|
||||
... # doctest: +ELLIPSIS
|
||||
[(0.994, 3.0], (5.0, 7.0], (3.0, 5.0], (3.0, 5.0], (5.0, 7.0], ...
|
||||
Categories (3, interval[float64]): [(0.994, 3.0] < (3.0, 5.0] ...
|
||||
|
||||
>>> pd.cut(np.array([1, 7, 5, 4, 6, 3]), 3, retbins=True)
|
||||
... # doctest: +ELLIPSIS
|
||||
([(0.994, 3.0], (5.0, 7.0], (3.0, 5.0], (3.0, 5.0], (5.0, 7.0], ...
|
||||
Categories (3, interval[float64]): [(0.994, 3.0] < (3.0, 5.0] ...
|
||||
array([0.994, 3. , 5. , 7. ]))
|
||||
|
||||
Discovers the same bins, but assign them specific labels. Notice that
|
||||
the returned Categorical's categories are `labels` and is ordered.
|
||||
|
||||
>>> pd.cut(np.array([1, 7, 5, 4, 6, 3]),
|
||||
... 3, labels=["bad", "medium", "good"])
|
||||
[bad, good, medium, medium, good, bad]
|
||||
Categories (3, object): [bad < medium < good]
|
||||
|
||||
``labels=False`` implies you just want the bins back.
|
||||
|
||||
>>> pd.cut([0, 1, 1, 2], bins=4, labels=False)
|
||||
array([0, 1, 1, 3])
|
||||
|
||||
Passing a Series as an input returns a Series with categorical dtype:
|
||||
|
||||
>>> s = pd.Series(np.array([2, 4, 6, 8, 10]),
|
||||
... index=['a', 'b', 'c', 'd', 'e'])
|
||||
>>> pd.cut(s, 3)
|
||||
... # doctest: +ELLIPSIS
|
||||
a (1.992, 4.667]
|
||||
b (1.992, 4.667]
|
||||
c (4.667, 7.333]
|
||||
d (7.333, 10.0]
|
||||
e (7.333, 10.0]
|
||||
dtype: category
|
||||
Categories (3, interval[float64]): [(1.992, 4.667] < (4.667, ...
|
||||
|
||||
Passing a Series as an input returns a Series with mapping value.
|
||||
It is used to map numerically to intervals based on bins.
|
||||
|
||||
>>> s = pd.Series(np.array([2, 4, 6, 8, 10]),
|
||||
... index=['a', 'b', 'c', 'd', 'e'])
|
||||
>>> pd.cut(s, [0, 2, 4, 6, 8, 10], labels=False, retbins=True, right=False)
|
||||
... # doctest: +ELLIPSIS
|
||||
(a 0.0
|
||||
b 1.0
|
||||
c 2.0
|
||||
d 3.0
|
||||
e 4.0
|
||||
dtype: float64, array([0, 2, 4, 6, 8]))
|
||||
|
||||
Use `drop` optional when bins is not unique
|
||||
|
||||
>>> pd.cut(s, [0, 2, 4, 6, 10, 10], labels=False, retbins=True,
|
||||
... right=False, duplicates='drop')
|
||||
... # doctest: +ELLIPSIS
|
||||
(a 0.0
|
||||
b 1.0
|
||||
c 2.0
|
||||
d 3.0
|
||||
e 3.0
|
||||
dtype: float64, array([0, 2, 4, 6, 8]))
|
||||
|
||||
Passing an IntervalIndex for `bins` results in those categories exactly.
|
||||
Notice that values not covered by the IntervalIndex are set to NaN. 0
|
||||
is to the left of the first bin (which is closed on the right), and 1.5
|
||||
falls between two bins.
|
||||
|
||||
>>> bins = pd.IntervalIndex.from_tuples([(0, 1), (2, 3), (4, 5)])
|
||||
>>> pd.cut([0, 0.5, 1.5, 2.5, 4.5], bins)
|
||||
[NaN, (0, 1], NaN, (2, 3], (4, 5]]
|
||||
Categories (3, interval[int64]): [(0, 1] < (2, 3] < (4, 5]]
|
||||
"""
|
||||
# NOTE: this binning code is changed a bit from histogram for var(x) == 0
|
||||
|
||||
# for handling the cut for datetime and timedelta objects
|
||||
x_is_series, series_index, name, x = _preprocess_for_cut(x)
|
||||
x, dtype = _coerce_to_type(x)
|
||||
|
||||
if not np.iterable(bins):
|
||||
if is_scalar(bins) and bins < 1:
|
||||
raise ValueError("`bins` should be a positive integer.")
|
||||
|
||||
try: # for array-like
|
||||
sz = x.size
|
||||
except AttributeError:
|
||||
x = np.asarray(x)
|
||||
sz = x.size
|
||||
|
||||
if sz == 0:
|
||||
raise ValueError('Cannot cut empty array')
|
||||
|
||||
rng = (nanops.nanmin(x), nanops.nanmax(x))
|
||||
mn, mx = [mi + 0.0 for mi in rng]
|
||||
|
||||
if mn == mx: # adjust end points before binning
|
||||
mn -= .001 * abs(mn) if mn != 0 else .001
|
||||
mx += .001 * abs(mx) if mx != 0 else .001
|
||||
bins = np.linspace(mn, mx, bins + 1, endpoint=True)
|
||||
else: # adjust end points after binning
|
||||
bins = np.linspace(mn, mx, bins + 1, endpoint=True)
|
||||
adj = (mx - mn) * 0.001 # 0.1% of the range
|
||||
if right:
|
||||
bins[0] -= adj
|
||||
else:
|
||||
bins[-1] += adj
|
||||
|
||||
elif isinstance(bins, IntervalIndex):
|
||||
pass
|
||||
else:
|
||||
bins = np.asarray(bins)
|
||||
bins = _convert_bin_to_numeric_type(bins, dtype)
|
||||
if (np.diff(bins) < 0).any():
|
||||
raise ValueError('bins must increase monotonically.')
|
||||
|
||||
fac, bins = _bins_to_cuts(x, bins, right=right, labels=labels,
|
||||
precision=precision,
|
||||
include_lowest=include_lowest,
|
||||
dtype=dtype,
|
||||
duplicates=duplicates)
|
||||
|
||||
return _postprocess_for_cut(fac, bins, retbins, x_is_series,
|
||||
series_index, name)
|
||||
|
||||
|
||||
def qcut(x, q, labels=None, retbins=False, precision=3, duplicates='raise'):
|
||||
"""
|
||||
Quantile-based discretization function. Discretize variable into
|
||||
equal-sized buckets based on rank or based on sample quantiles. For example
|
||||
1000 values for 10 quantiles would produce a Categorical object indicating
|
||||
quantile membership for each data point.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : 1d ndarray or Series
|
||||
q : integer or array of quantiles
|
||||
Number of quantiles. 10 for deciles, 4 for quartiles, etc. Alternately
|
||||
array of quantiles, e.g. [0, .25, .5, .75, 1.] for quartiles
|
||||
labels : array or boolean, default None
|
||||
Used as labels for the resulting bins. Must be of the same length as
|
||||
the resulting bins. If False, return only integer indicators of the
|
||||
bins.
|
||||
retbins : bool, optional
|
||||
Whether to return the (bins, labels) or not. Can be useful if bins
|
||||
is given as a scalar.
|
||||
precision : int, optional
|
||||
The precision at which to store and display the bins labels
|
||||
duplicates : {default 'raise', 'drop'}, optional
|
||||
If bin edges are not unique, raise ValueError or drop non-uniques.
|
||||
|
||||
.. versionadded:: 0.20.0
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : Categorical or Series or array of integers if labels is False
|
||||
The return type (Categorical or Series) depends on the input: a Series
|
||||
of type category if input is a Series else Categorical. Bins are
|
||||
represented as categories when categorical data is returned.
|
||||
bins : ndarray of floats
|
||||
Returned only if `retbins` is True.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Out of bounds values will be NA in the resulting Categorical object
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> pd.qcut(range(5), 4)
|
||||
... # doctest: +ELLIPSIS
|
||||
[(-0.001, 1.0], (-0.001, 1.0], (1.0, 2.0], (2.0, 3.0], (3.0, 4.0]]
|
||||
Categories (4, interval[float64]): [(-0.001, 1.0] < (1.0, 2.0] ...
|
||||
|
||||
>>> pd.qcut(range(5), 3, labels=["good", "medium", "bad"])
|
||||
... # doctest: +SKIP
|
||||
[good, good, medium, bad, bad]
|
||||
Categories (3, object): [good < medium < bad]
|
||||
|
||||
>>> pd.qcut(range(5), 4, labels=False)
|
||||
array([0, 0, 1, 2, 3])
|
||||
"""
|
||||
x_is_series, series_index, name, x = _preprocess_for_cut(x)
|
||||
|
||||
x, dtype = _coerce_to_type(x)
|
||||
|
||||
if is_integer(q):
|
||||
quantiles = np.linspace(0, 1, q + 1)
|
||||
else:
|
||||
quantiles = q
|
||||
bins = algos.quantile(x, quantiles)
|
||||
fac, bins = _bins_to_cuts(x, bins, labels=labels,
|
||||
precision=precision, include_lowest=True,
|
||||
dtype=dtype, duplicates=duplicates)
|
||||
|
||||
return _postprocess_for_cut(fac, bins, retbins, x_is_series,
|
||||
series_index, name)
|
||||
|
||||
|
||||
def _bins_to_cuts(x, bins, right=True, labels=None,
|
||||
precision=3, include_lowest=False,
|
||||
dtype=None, duplicates='raise'):
|
||||
|
||||
if duplicates not in ['raise', 'drop']:
|
||||
raise ValueError("invalid value for 'duplicates' parameter, "
|
||||
"valid options are: raise, drop")
|
||||
|
||||
if isinstance(bins, IntervalIndex):
|
||||
# we have a fast-path here
|
||||
ids = bins.get_indexer(x)
|
||||
result = algos.take_nd(bins, ids)
|
||||
result = Categorical(result, categories=bins, ordered=True)
|
||||
return result, bins
|
||||
|
||||
unique_bins = algos.unique(bins)
|
||||
if len(unique_bins) < len(bins) and len(bins) != 2:
|
||||
if duplicates == 'raise':
|
||||
raise ValueError("Bin edges must be unique: {bins!r}.\nYou "
|
||||
"can drop duplicate edges by setting "
|
||||
"the 'duplicates' kwarg".format(bins=bins))
|
||||
else:
|
||||
bins = unique_bins
|
||||
|
||||
side = 'left' if right else 'right'
|
||||
ids = _ensure_int64(bins.searchsorted(x, side=side))
|
||||
|
||||
if include_lowest:
|
||||
# Numpy 1.9 support: ensure this mask is a Numpy array
|
||||
ids[np.asarray(x == bins[0])] = 1
|
||||
|
||||
na_mask = isna(x) | (ids == len(bins)) | (ids == 0)
|
||||
has_nas = na_mask.any()
|
||||
|
||||
if labels is not False:
|
||||
if labels is None:
|
||||
labels = _format_labels(bins, precision, right=right,
|
||||
include_lowest=include_lowest,
|
||||
dtype=dtype)
|
||||
else:
|
||||
if len(labels) != len(bins) - 1:
|
||||
raise ValueError('Bin labels must be one fewer than '
|
||||
'the number of bin edges')
|
||||
if not is_categorical_dtype(labels):
|
||||
labels = Categorical(labels, categories=labels, ordered=True)
|
||||
|
||||
np.putmask(ids, na_mask, 0)
|
||||
result = algos.take_nd(labels, ids - 1)
|
||||
|
||||
else:
|
||||
result = ids - 1
|
||||
if has_nas:
|
||||
result = result.astype(np.float64)
|
||||
np.putmask(result, na_mask, np.nan)
|
||||
|
||||
return result, bins
|
||||
|
||||
|
||||
def _trim_zeros(x):
|
||||
while len(x) > 1 and x[-1] == '0':
|
||||
x = x[:-1]
|
||||
if len(x) > 1 and x[-1] == '.':
|
||||
x = x[:-1]
|
||||
return x
|
||||
|
||||
|
||||
def _coerce_to_type(x):
|
||||
"""
|
||||
if the passed data is of datetime/timedelta type,
|
||||
this method converts it to numeric so that cut method can
|
||||
handle it
|
||||
"""
|
||||
dtype = None
|
||||
|
||||
if is_datetime64tz_dtype(x):
|
||||
dtype = x.dtype
|
||||
elif is_datetime64_dtype(x):
|
||||
x = to_datetime(x)
|
||||
dtype = np.datetime64
|
||||
elif is_timedelta64_dtype(x):
|
||||
x = to_timedelta(x)
|
||||
dtype = np.timedelta64
|
||||
|
||||
if dtype is not None:
|
||||
# GH 19768: force NaT to NaN during integer conversion
|
||||
x = np.where(x.notna(), x.view(np.int64), np.nan)
|
||||
|
||||
return x, dtype
|
||||
|
||||
|
||||
def _convert_bin_to_numeric_type(bins, dtype):
|
||||
"""
|
||||
if the passed bin is of datetime/timedelta type,
|
||||
this method converts it to integer
|
||||
|
||||
Parameters
|
||||
----------
|
||||
bins : list-like of bins
|
||||
dtype : dtype of data
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError if bins are not of a compat dtype to dtype
|
||||
"""
|
||||
bins_dtype = infer_dtype(bins)
|
||||
if is_timedelta64_dtype(dtype):
|
||||
if bins_dtype in ['timedelta', 'timedelta64']:
|
||||
bins = to_timedelta(bins).view(np.int64)
|
||||
else:
|
||||
raise ValueError("bins must be of timedelta64 dtype")
|
||||
elif is_datetime64_dtype(dtype) or is_datetime64tz_dtype(dtype):
|
||||
if bins_dtype in ['datetime', 'datetime64']:
|
||||
bins = to_datetime(bins).view(np.int64)
|
||||
else:
|
||||
raise ValueError("bins must be of datetime64 dtype")
|
||||
|
||||
return bins
|
||||
|
||||
|
||||
def _format_labels(bins, precision, right=True,
|
||||
include_lowest=False, dtype=None):
|
||||
""" based on the dtype, return our labels """
|
||||
|
||||
closed = 'right' if right else 'left'
|
||||
|
||||
if is_datetime64tz_dtype(dtype):
|
||||
formatter = partial(Timestamp, tz=dtype.tz)
|
||||
adjust = lambda x: x - Timedelta('1ns')
|
||||
elif is_datetime64_dtype(dtype):
|
||||
formatter = Timestamp
|
||||
adjust = lambda x: x - Timedelta('1ns')
|
||||
elif is_timedelta64_dtype(dtype):
|
||||
formatter = Timedelta
|
||||
adjust = lambda x: x - Timedelta('1ns')
|
||||
else:
|
||||
precision = _infer_precision(precision, bins)
|
||||
formatter = lambda x: _round_frac(x, precision)
|
||||
adjust = lambda x: x - 10 ** (-precision)
|
||||
|
||||
breaks = [formatter(b) for b in bins]
|
||||
labels = IntervalIndex.from_breaks(breaks, closed=closed)
|
||||
|
||||
if right and include_lowest:
|
||||
# we will adjust the left hand side by precision to
|
||||
# account that we are all right closed
|
||||
v = adjust(labels[0].left)
|
||||
|
||||
i = IntervalIndex([Interval(v, labels[0].right, closed='right')])
|
||||
labels = i.append(labels[1:])
|
||||
|
||||
return labels
|
||||
|
||||
|
||||
def _preprocess_for_cut(x):
|
||||
"""
|
||||
handles preprocessing for cut where we convert passed
|
||||
input to array, strip the index information and store it
|
||||
separately
|
||||
"""
|
||||
x_is_series = isinstance(x, Series)
|
||||
series_index = None
|
||||
name = None
|
||||
|
||||
if x_is_series:
|
||||
series_index = x.index
|
||||
name = x.name
|
||||
|
||||
# Check that the passed array is a Pandas or Numpy object
|
||||
# We don't want to strip away a Pandas data-type here (e.g. datetimetz)
|
||||
ndim = getattr(x, 'ndim', None)
|
||||
if ndim is None:
|
||||
x = np.asarray(x)
|
||||
if x.ndim != 1:
|
||||
raise ValueError("Input array must be 1 dimensional")
|
||||
|
||||
return x_is_series, series_index, name, x
|
||||
|
||||
|
||||
def _postprocess_for_cut(fac, bins, retbins, x_is_series,
|
||||
series_index, name):
|
||||
"""
|
||||
handles post processing for the cut method where
|
||||
we combine the index information if the originally passed
|
||||
datatype was a series
|
||||
"""
|
||||
if x_is_series:
|
||||
fac = Series(fac, index=series_index, name=name)
|
||||
|
||||
if not retbins:
|
||||
return fac
|
||||
|
||||
return fac, bins
|
||||
|
||||
|
||||
def _round_frac(x, precision):
|
||||
"""
|
||||
Round the fractional part of the given number
|
||||
"""
|
||||
if not np.isfinite(x) or x == 0:
|
||||
return x
|
||||
else:
|
||||
frac, whole = np.modf(x)
|
||||
if whole == 0:
|
||||
digits = -int(np.floor(np.log10(abs(frac)))) - 1 + precision
|
||||
else:
|
||||
digits = precision
|
||||
return np.around(x, digits)
|
||||
|
||||
|
||||
def _infer_precision(base_precision, bins):
|
||||
"""Infer an appropriate precision for _round_frac
|
||||
"""
|
||||
for precision in range(base_precision, 20):
|
||||
levels = [_round_frac(b, precision) for b in bins]
|
||||
if algos.unique(levels).size == bins.size:
|
||||
return precision
|
||||
return base_precision # default
|
||||
@@ -0,0 +1,76 @@
|
||||
import numpy as np
|
||||
|
||||
from pandas.core.dtypes.common import is_list_like
|
||||
|
||||
from pandas.compat import reduce
|
||||
from pandas.core.index import Index
|
||||
from pandas.core import common as com
|
||||
|
||||
|
||||
def match(needles, haystack):
|
||||
haystack = Index(haystack)
|
||||
needles = Index(needles)
|
||||
return haystack.get_indexer(needles)
|
||||
|
||||
|
||||
def cartesian_product(X):
|
||||
"""
|
||||
Numpy version of itertools.product or pandas.compat.product.
|
||||
Sometimes faster (for large inputs)...
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : list-like of list-likes
|
||||
|
||||
Returns
|
||||
-------
|
||||
product : list of ndarrays
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> cartesian_product([list('ABC'), [1, 2]])
|
||||
[array(['A', 'A', 'B', 'B', 'C', 'C'], dtype='|S1'),
|
||||
array([1, 2, 1, 2, 1, 2])]
|
||||
|
||||
See also
|
||||
--------
|
||||
itertools.product : Cartesian product of input iterables. Equivalent to
|
||||
nested for-loops.
|
||||
pandas.compat.product : An alias for itertools.product.
|
||||
"""
|
||||
msg = "Input must be a list-like of list-likes"
|
||||
if not is_list_like(X):
|
||||
raise TypeError(msg)
|
||||
for x in X:
|
||||
if not is_list_like(x):
|
||||
raise TypeError(msg)
|
||||
|
||||
if len(X) == 0:
|
||||
return []
|
||||
|
||||
lenX = np.fromiter((len(x) for x in X), dtype=np.intp)
|
||||
cumprodX = np.cumproduct(lenX)
|
||||
|
||||
a = np.roll(cumprodX, 1)
|
||||
a[0] = 1
|
||||
|
||||
if cumprodX[-1] != 0:
|
||||
b = cumprodX[-1] / cumprodX
|
||||
else:
|
||||
# if any factor is empty, the cartesian product is empty
|
||||
b = np.zeros_like(cumprodX)
|
||||
|
||||
return [np.tile(np.repeat(np.asarray(com._values_from_object(x)), b[i]),
|
||||
np.product(a[i]))
|
||||
for i, x in enumerate(X)]
|
||||
|
||||
|
||||
def _compose2(f, g):
|
||||
"""Compose 2 callables"""
|
||||
return lambda *args, **kwargs: f(g(*args, **kwargs))
|
||||
|
||||
|
||||
def compose(*funcs):
|
||||
"""Compose 2 or more callables"""
|
||||
assert len(funcs) > 1, 'At least 2 callables must be passed to compose'
|
||||
return reduce(_compose2, funcs)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,490 @@
|
||||
""" miscellaneous sorting / groupby utilities """
|
||||
|
||||
import numpy as np
|
||||
from pandas.compat import long, string_types, PY3
|
||||
from pandas.core.dtypes.common import (
|
||||
_ensure_platform_int,
|
||||
_ensure_int64,
|
||||
is_list_like,
|
||||
is_categorical_dtype)
|
||||
from pandas.core.dtypes.cast import infer_dtype_from_array
|
||||
from pandas.core.dtypes.missing import isna
|
||||
import pandas.core.algorithms as algorithms
|
||||
from pandas._libs import lib, algos, hashtable
|
||||
from pandas._libs.hashtable import unique_label_indices
|
||||
|
||||
|
||||
_INT64_MAX = np.iinfo(np.int64).max
|
||||
|
||||
|
||||
def get_group_index(labels, shape, sort, xnull):
|
||||
"""
|
||||
For the particular label_list, gets the offsets into the hypothetical list
|
||||
representing the totally ordered cartesian product of all possible label
|
||||
combinations, *as long as* this space fits within int64 bounds;
|
||||
otherwise, though group indices identify unique combinations of
|
||||
labels, they cannot be deconstructed.
|
||||
- If `sort`, rank of returned ids preserve lexical ranks of labels.
|
||||
i.e. returned id's can be used to do lexical sort on labels;
|
||||
- If `xnull` nulls (-1 labels) are passed through.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
labels: sequence of arrays
|
||||
Integers identifying levels at each location
|
||||
shape: sequence of ints same length as labels
|
||||
Number of unique levels at each location
|
||||
sort: boolean
|
||||
If the ranks of returned ids should match lexical ranks of labels
|
||||
xnull: boolean
|
||||
If true nulls are excluded. i.e. -1 values in the labels are
|
||||
passed through
|
||||
Returns
|
||||
-------
|
||||
An array of type int64 where two elements are equal if their corresponding
|
||||
labels are equal at all location.
|
||||
"""
|
||||
def _int64_cut_off(shape):
|
||||
acc = long(1)
|
||||
for i, mul in enumerate(shape):
|
||||
acc *= long(mul)
|
||||
if not acc < _INT64_MAX:
|
||||
return i
|
||||
return len(shape)
|
||||
|
||||
def maybe_lift(lab, size):
|
||||
# promote nan values (assigned -1 label in lab array)
|
||||
# so that all output values are non-negative
|
||||
return (lab + 1, size + 1) if (lab == -1).any() else (lab, size)
|
||||
|
||||
labels = map(_ensure_int64, labels)
|
||||
if not xnull:
|
||||
labels, shape = map(list, zip(*map(maybe_lift, labels, shape)))
|
||||
|
||||
labels = list(labels)
|
||||
shape = list(shape)
|
||||
|
||||
# Iteratively process all the labels in chunks sized so less
|
||||
# than _INT64_MAX unique int ids will be required for each chunk
|
||||
while True:
|
||||
# how many levels can be done without overflow:
|
||||
nlev = _int64_cut_off(shape)
|
||||
|
||||
# compute flat ids for the first `nlev` levels
|
||||
stride = np.prod(shape[1:nlev], dtype='i8')
|
||||
out = stride * labels[0].astype('i8', subok=False, copy=False)
|
||||
|
||||
for i in range(1, nlev):
|
||||
if shape[i] == 0:
|
||||
stride = 0
|
||||
else:
|
||||
stride //= shape[i]
|
||||
out += labels[i] * stride
|
||||
|
||||
if xnull: # exclude nulls
|
||||
mask = labels[0] == -1
|
||||
for lab in labels[1:nlev]:
|
||||
mask |= lab == -1
|
||||
out[mask] = -1
|
||||
|
||||
if nlev == len(shape): # all levels done!
|
||||
break
|
||||
|
||||
# compress what has been done so far in order to avoid overflow
|
||||
# to retain lexical ranks, obs_ids should be sorted
|
||||
comp_ids, obs_ids = compress_group_index(out, sort=sort)
|
||||
|
||||
labels = [comp_ids] + labels[nlev:]
|
||||
shape = [len(obs_ids)] + shape[nlev:]
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def get_compressed_ids(labels, sizes):
|
||||
"""
|
||||
|
||||
Group_index is offsets into cartesian product of all possible labels. This
|
||||
space can be huge, so this function compresses it, by computing offsets
|
||||
(comp_ids) into the list of unique labels (obs_group_ids).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
labels : list of label arrays
|
||||
sizes : list of size of the levels
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple of (comp_ids, obs_group_ids)
|
||||
|
||||
"""
|
||||
ids = get_group_index(labels, sizes, sort=True, xnull=False)
|
||||
return compress_group_index(ids, sort=True)
|
||||
|
||||
|
||||
def is_int64_overflow_possible(shape):
|
||||
the_prod = long(1)
|
||||
for x in shape:
|
||||
the_prod *= long(x)
|
||||
|
||||
return the_prod >= _INT64_MAX
|
||||
|
||||
|
||||
def decons_group_index(comp_labels, shape):
|
||||
# reconstruct labels
|
||||
if is_int64_overflow_possible(shape):
|
||||
# at some point group indices are factorized,
|
||||
# and may not be deconstructed here! wrong path!
|
||||
raise ValueError('cannot deconstruct factorized group indices!')
|
||||
|
||||
label_list = []
|
||||
factor = 1
|
||||
y = 0
|
||||
x = comp_labels
|
||||
for i in reversed(range(len(shape))):
|
||||
labels = (x - y) % (factor * shape[i]) // factor
|
||||
np.putmask(labels, comp_labels < 0, -1)
|
||||
label_list.append(labels)
|
||||
y = labels * factor
|
||||
factor *= shape[i]
|
||||
return label_list[::-1]
|
||||
|
||||
|
||||
def decons_obs_group_ids(comp_ids, obs_ids, shape, labels, xnull):
|
||||
"""
|
||||
reconstruct labels from observed group ids
|
||||
|
||||
Parameters
|
||||
----------
|
||||
xnull: boolean,
|
||||
if nulls are excluded; i.e. -1 labels are passed through
|
||||
"""
|
||||
|
||||
if not xnull:
|
||||
lift = np.fromiter(((a == -1).any() for a in labels), dtype='i8')
|
||||
shape = np.asarray(shape, dtype='i8') + lift
|
||||
|
||||
if not is_int64_overflow_possible(shape):
|
||||
# obs ids are deconstructable! take the fast route!
|
||||
out = decons_group_index(obs_ids, shape)
|
||||
return out if xnull or not lift.any() \
|
||||
else [x - y for x, y in zip(out, lift)]
|
||||
|
||||
i = unique_label_indices(comp_ids)
|
||||
i8copy = lambda a: a.astype('i8', subok=False, copy=True)
|
||||
return [i8copy(lab[i]) for lab in labels]
|
||||
|
||||
|
||||
def indexer_from_factorized(labels, shape, compress=True):
|
||||
ids = get_group_index(labels, shape, sort=True, xnull=False)
|
||||
|
||||
if not compress:
|
||||
ngroups = (ids.size and ids.max()) + 1
|
||||
else:
|
||||
ids, obs = compress_group_index(ids, sort=True)
|
||||
ngroups = len(obs)
|
||||
|
||||
return get_group_index_sorter(ids, ngroups)
|
||||
|
||||
|
||||
def lexsort_indexer(keys, orders=None, na_position='last'):
|
||||
from pandas.core.arrays import Categorical
|
||||
|
||||
labels = []
|
||||
shape = []
|
||||
if isinstance(orders, bool):
|
||||
orders = [orders] * len(keys)
|
||||
elif orders is None:
|
||||
orders = [True] * len(keys)
|
||||
|
||||
for key, order in zip(keys, orders):
|
||||
|
||||
# we are already a Categorical
|
||||
if is_categorical_dtype(key):
|
||||
c = key
|
||||
|
||||
# create the Categorical
|
||||
else:
|
||||
c = Categorical(key, ordered=True)
|
||||
|
||||
if na_position not in ['last', 'first']:
|
||||
raise ValueError('invalid na_position: {!r}'.format(na_position))
|
||||
|
||||
n = len(c.categories)
|
||||
codes = c.codes.copy()
|
||||
|
||||
mask = (c.codes == -1)
|
||||
if order: # ascending
|
||||
if na_position == 'last':
|
||||
codes = np.where(mask, n, codes)
|
||||
elif na_position == 'first':
|
||||
codes += 1
|
||||
else: # not order means descending
|
||||
if na_position == 'last':
|
||||
codes = np.where(mask, n, n - codes - 1)
|
||||
elif na_position == 'first':
|
||||
codes = np.where(mask, 0, n - codes)
|
||||
if mask.any():
|
||||
n += 1
|
||||
|
||||
shape.append(n)
|
||||
labels.append(codes)
|
||||
|
||||
return indexer_from_factorized(labels, shape)
|
||||
|
||||
|
||||
def nargsort(items, kind='quicksort', ascending=True, na_position='last'):
|
||||
"""
|
||||
This is intended to be a drop-in replacement for np.argsort which
|
||||
handles NaNs. It adds ascending and na_position parameters.
|
||||
GH #6399, #5231
|
||||
"""
|
||||
|
||||
# specially handle Categorical
|
||||
if is_categorical_dtype(items):
|
||||
return items.argsort(ascending=ascending, kind=kind)
|
||||
|
||||
items = np.asanyarray(items)
|
||||
idx = np.arange(len(items))
|
||||
mask = isna(items)
|
||||
non_nans = items[~mask]
|
||||
non_nan_idx = idx[~mask]
|
||||
nan_idx = np.nonzero(mask)[0]
|
||||
if not ascending:
|
||||
non_nans = non_nans[::-1]
|
||||
non_nan_idx = non_nan_idx[::-1]
|
||||
indexer = non_nan_idx[non_nans.argsort(kind=kind)]
|
||||
if not ascending:
|
||||
indexer = indexer[::-1]
|
||||
# Finally, place the NaNs at the end or the beginning according to
|
||||
# na_position
|
||||
if na_position == 'last':
|
||||
indexer = np.concatenate([indexer, nan_idx])
|
||||
elif na_position == 'first':
|
||||
indexer = np.concatenate([nan_idx, indexer])
|
||||
else:
|
||||
raise ValueError('invalid na_position: {!r}'.format(na_position))
|
||||
return indexer
|
||||
|
||||
|
||||
class _KeyMapper(object):
|
||||
|
||||
"""
|
||||
Ease my suffering. Map compressed group id -> key tuple
|
||||
"""
|
||||
|
||||
def __init__(self, comp_ids, ngroups, levels, labels):
|
||||
self.levels = levels
|
||||
self.labels = labels
|
||||
self.comp_ids = comp_ids.astype(np.int64)
|
||||
|
||||
self.k = len(labels)
|
||||
self.tables = [hashtable.Int64HashTable(ngroups)
|
||||
for _ in range(self.k)]
|
||||
|
||||
self._populate_tables()
|
||||
|
||||
def _populate_tables(self):
|
||||
for labs, table in zip(self.labels, self.tables):
|
||||
table.map(self.comp_ids, labs.astype(np.int64))
|
||||
|
||||
def get_key(self, comp_id):
|
||||
return tuple(level[table.get_item(comp_id)]
|
||||
for table, level in zip(self.tables, self.levels))
|
||||
|
||||
|
||||
def get_flattened_iterator(comp_ids, ngroups, levels, labels):
|
||||
# provide "flattened" iterator for multi-group setting
|
||||
mapper = _KeyMapper(comp_ids, ngroups, levels, labels)
|
||||
return [mapper.get_key(i) for i in range(ngroups)]
|
||||
|
||||
|
||||
def get_indexer_dict(label_list, keys):
|
||||
""" return a diction of {labels} -> {indexers} """
|
||||
shape = list(map(len, keys))
|
||||
|
||||
group_index = get_group_index(label_list, shape, sort=True, xnull=True)
|
||||
ngroups = ((group_index.size and group_index.max()) + 1) \
|
||||
if is_int64_overflow_possible(shape) \
|
||||
else np.prod(shape, dtype='i8')
|
||||
|
||||
sorter = get_group_index_sorter(group_index, ngroups)
|
||||
|
||||
sorted_labels = [lab.take(sorter) for lab in label_list]
|
||||
group_index = group_index.take(sorter)
|
||||
|
||||
return lib.indices_fast(sorter, group_index, keys, sorted_labels)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# sorting levels...cleverly?
|
||||
|
||||
def get_group_index_sorter(group_index, ngroups):
|
||||
"""
|
||||
algos.groupsort_indexer implements `counting sort` and it is at least
|
||||
O(ngroups), where
|
||||
ngroups = prod(shape)
|
||||
shape = map(len, keys)
|
||||
that is, linear in the number of combinations (cartesian product) of unique
|
||||
values of groupby keys. This can be huge when doing multi-key groupby.
|
||||
np.argsort(kind='mergesort') is O(count x log(count)) where count is the
|
||||
length of the data-frame;
|
||||
Both algorithms are `stable` sort and that is necessary for correctness of
|
||||
groupby operations. e.g. consider:
|
||||
df.groupby(key)[col].transform('first')
|
||||
"""
|
||||
count = len(group_index)
|
||||
alpha = 0.0 # taking complexities literally; there may be
|
||||
beta = 1.0 # some room for fine-tuning these parameters
|
||||
do_groupsort = (count > 0 and ((alpha + beta * ngroups) <
|
||||
(count * np.log(count))))
|
||||
if do_groupsort:
|
||||
sorter, _ = algos.groupsort_indexer(_ensure_int64(group_index),
|
||||
ngroups)
|
||||
return _ensure_platform_int(sorter)
|
||||
else:
|
||||
return group_index.argsort(kind='mergesort')
|
||||
|
||||
|
||||
def compress_group_index(group_index, sort=True):
|
||||
"""
|
||||
Group_index is offsets into cartesian product of all possible labels. This
|
||||
space can be huge, so this function compresses it, by computing offsets
|
||||
(comp_ids) into the list of unique labels (obs_group_ids).
|
||||
"""
|
||||
|
||||
size_hint = min(len(group_index), hashtable._SIZE_HINT_LIMIT)
|
||||
table = hashtable.Int64HashTable(size_hint)
|
||||
|
||||
group_index = _ensure_int64(group_index)
|
||||
|
||||
# note, group labels come out ascending (ie, 1,2,3 etc)
|
||||
comp_ids, obs_group_ids = table.get_labels_groupby(group_index)
|
||||
|
||||
if sort and len(obs_group_ids) > 0:
|
||||
obs_group_ids, comp_ids = _reorder_by_uniques(obs_group_ids, comp_ids)
|
||||
|
||||
return comp_ids, obs_group_ids
|
||||
|
||||
|
||||
def _reorder_by_uniques(uniques, labels):
|
||||
# sorter is index where elements ought to go
|
||||
sorter = uniques.argsort()
|
||||
|
||||
# reverse_indexer is where elements came from
|
||||
reverse_indexer = np.empty(len(sorter), dtype=np.int64)
|
||||
reverse_indexer.put(sorter, np.arange(len(sorter)))
|
||||
|
||||
mask = labels < 0
|
||||
|
||||
# move labels to right locations (ie, unsort ascending labels)
|
||||
labels = algorithms.take_nd(reverse_indexer, labels, allow_fill=False)
|
||||
np.putmask(labels, mask, -1)
|
||||
|
||||
# sort observed ids
|
||||
uniques = algorithms.take_nd(uniques, sorter, allow_fill=False)
|
||||
|
||||
return uniques, labels
|
||||
|
||||
|
||||
def safe_sort(values, labels=None, na_sentinel=-1, assume_unique=False):
|
||||
"""
|
||||
Sort ``values`` and reorder corresponding ``labels``.
|
||||
``values`` should be unique if ``labels`` is not None.
|
||||
Safe for use with mixed types (int, str), orders ints before strs.
|
||||
|
||||
.. versionadded:: 0.19.0
|
||||
|
||||
Parameters
|
||||
----------
|
||||
values : list-like
|
||||
Sequence; must be unique if ``labels`` is not None.
|
||||
labels : list_like
|
||||
Indices to ``values``. All out of bound indices are treated as
|
||||
"not found" and will be masked with ``na_sentinel``.
|
||||
na_sentinel : int, default -1
|
||||
Value in ``labels`` to mark "not found".
|
||||
Ignored when ``labels`` is None.
|
||||
assume_unique : bool, default False
|
||||
When True, ``values`` are assumed to be unique, which can speed up
|
||||
the calculation. Ignored when ``labels`` is None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ordered : ndarray
|
||||
Sorted ``values``
|
||||
new_labels : ndarray
|
||||
Reordered ``labels``; returned when ``labels`` is not None.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
* If ``values`` is not list-like or if ``labels`` is neither None
|
||||
nor list-like
|
||||
* If ``values`` cannot be sorted
|
||||
ValueError
|
||||
* If ``labels`` is not None and ``values`` contain duplicates.
|
||||
"""
|
||||
if not is_list_like(values):
|
||||
raise TypeError("Only list-like objects are allowed to be passed to"
|
||||
"safe_sort as values")
|
||||
|
||||
if not isinstance(values, np.ndarray):
|
||||
|
||||
# don't convert to string types
|
||||
dtype, _ = infer_dtype_from_array(values)
|
||||
values = np.asarray(values, dtype=dtype)
|
||||
|
||||
def sort_mixed(values):
|
||||
# order ints before strings, safe in py3
|
||||
str_pos = np.array([isinstance(x, string_types) for x in values],
|
||||
dtype=bool)
|
||||
nums = np.sort(values[~str_pos])
|
||||
strs = np.sort(values[str_pos])
|
||||
return np.concatenate([nums, np.asarray(strs, dtype=object)])
|
||||
|
||||
sorter = None
|
||||
if PY3 and lib.infer_dtype(values) == 'mixed-integer':
|
||||
# unorderable in py3 if mixed str/int
|
||||
ordered = sort_mixed(values)
|
||||
else:
|
||||
try:
|
||||
sorter = values.argsort()
|
||||
ordered = values.take(sorter)
|
||||
except TypeError:
|
||||
# try this anyway
|
||||
ordered = sort_mixed(values)
|
||||
|
||||
# labels:
|
||||
|
||||
if labels is None:
|
||||
return ordered
|
||||
|
||||
if not is_list_like(labels):
|
||||
raise TypeError("Only list-like objects or None are allowed to be"
|
||||
"passed to safe_sort as labels")
|
||||
labels = _ensure_platform_int(np.asarray(labels))
|
||||
|
||||
from pandas import Index
|
||||
if not assume_unique and not Index(values).is_unique:
|
||||
raise ValueError("values should be unique if labels is not None")
|
||||
|
||||
if sorter is None:
|
||||
# mixed types
|
||||
(hash_klass, _), values = algorithms._get_data_algo(
|
||||
values, algorithms._hashtables)
|
||||
t = hash_klass(len(values))
|
||||
t.map_locations(values)
|
||||
sorter = _ensure_platform_int(t.lookup(ordered))
|
||||
|
||||
reverse_indexer = np.empty(len(sorter), dtype=np.int_)
|
||||
reverse_indexer.put(sorter, np.arange(len(sorter)))
|
||||
|
||||
mask = (labels < -len(values)) | (labels >= len(values)) | \
|
||||
(labels == na_sentinel)
|
||||
|
||||
# (Out of bound indices will be masked with `na_sentinel` next, so we may
|
||||
# deal with them here without performance loss using `mode='wrap'`.)
|
||||
new_labels = reverse_indexer.take(labels, mode='wrap')
|
||||
np.putmask(new_labels, mask, na_sentinel)
|
||||
|
||||
return ordered, _ensure_platform_int(new_labels)
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
# pylint: disable=W0611
|
||||
# flake8: noqa
|
||||
from pandas.core.sparse.array import SparseArray
|
||||
from pandas.core.sparse.series import SparseSeries
|
||||
from pandas.core.sparse.frame import SparseDataFrame
|
||||
@@ -0,0 +1,849 @@
|
||||
"""
|
||||
SparseArray data structure
|
||||
"""
|
||||
from __future__ import division
|
||||
# pylint: disable=E1101,E1103,W0231
|
||||
|
||||
import numpy as np
|
||||
import warnings
|
||||
|
||||
import pandas as pd
|
||||
from pandas.core.base import PandasObject, IndexOpsMixin
|
||||
|
||||
from pandas import compat
|
||||
from pandas.compat import range, PYPY
|
||||
from pandas.compat.numpy import function as nv
|
||||
|
||||
from pandas.core.dtypes.generic import ABCSparseSeries
|
||||
from pandas.core.dtypes.common import (
|
||||
_ensure_platform_int,
|
||||
is_float, is_integer,
|
||||
is_object_dtype,
|
||||
is_integer_dtype,
|
||||
is_bool_dtype,
|
||||
is_list_like,
|
||||
is_string_dtype,
|
||||
is_scalar, is_dtype_equal)
|
||||
from pandas.core.dtypes.cast import (
|
||||
maybe_convert_platform, maybe_promote,
|
||||
astype_nansafe, find_common_type, infer_dtype_from_scalar,
|
||||
construct_1d_arraylike_from_scalar)
|
||||
from pandas.core.dtypes.missing import isna, notna, na_value_for_dtype
|
||||
|
||||
import pandas._libs.sparse as splib
|
||||
import pandas._libs.lib as lib
|
||||
from pandas._libs.sparse import SparseIndex, BlockIndex, IntIndex
|
||||
from pandas._libs import index as libindex
|
||||
import pandas.core.algorithms as algos
|
||||
import pandas.core.ops as ops
|
||||
import pandas.io.formats.printing as printing
|
||||
from pandas.util._decorators import Appender
|
||||
from pandas.core.indexes.base import _index_shared_docs
|
||||
|
||||
|
||||
_sparray_doc_kwargs = dict(klass='SparseArray')
|
||||
|
||||
|
||||
def _get_fill(arr):
|
||||
# coerce fill_value to arr dtype if possible
|
||||
# int64 SparseArray can have NaN as fill_value if there is no missing
|
||||
try:
|
||||
return np.asarray(arr.fill_value, dtype=arr.dtype)
|
||||
except ValueError:
|
||||
return np.asarray(arr.fill_value)
|
||||
|
||||
|
||||
def _sparse_array_op(left, right, op, name):
|
||||
if name.startswith('__'):
|
||||
# For lookups in _libs.sparse we need non-dunder op name
|
||||
name = name[2:-2]
|
||||
|
||||
# dtype used to find corresponding sparse method
|
||||
if not is_dtype_equal(left.dtype, right.dtype):
|
||||
dtype = find_common_type([left.dtype, right.dtype])
|
||||
left = left.astype(dtype)
|
||||
right = right.astype(dtype)
|
||||
else:
|
||||
dtype = left.dtype
|
||||
|
||||
# dtype the result must have
|
||||
result_dtype = None
|
||||
|
||||
if left.sp_index.ngaps == 0 or right.sp_index.ngaps == 0:
|
||||
with np.errstate(all='ignore'):
|
||||
result = op(left.get_values(), right.get_values())
|
||||
fill = op(_get_fill(left), _get_fill(right))
|
||||
|
||||
if left.sp_index.ngaps == 0:
|
||||
index = left.sp_index
|
||||
else:
|
||||
index = right.sp_index
|
||||
elif left.sp_index.equals(right.sp_index):
|
||||
with np.errstate(all='ignore'):
|
||||
result = op(left.sp_values, right.sp_values)
|
||||
fill = op(_get_fill(left), _get_fill(right))
|
||||
index = left.sp_index
|
||||
else:
|
||||
if name[0] == 'r':
|
||||
left, right = right, left
|
||||
name = name[1:]
|
||||
|
||||
if name in ('and', 'or') and dtype == 'bool':
|
||||
opname = 'sparse_{name}_uint8'.format(name=name)
|
||||
# to make template simple, cast here
|
||||
left_sp_values = left.sp_values.view(np.uint8)
|
||||
right_sp_values = right.sp_values.view(np.uint8)
|
||||
result_dtype = np.bool
|
||||
else:
|
||||
opname = 'sparse_{name}_{dtype}'.format(name=name, dtype=dtype)
|
||||
left_sp_values = left.sp_values
|
||||
right_sp_values = right.sp_values
|
||||
|
||||
sparse_op = getattr(splib, opname)
|
||||
with np.errstate(all='ignore'):
|
||||
result, index, fill = sparse_op(left_sp_values, left.sp_index,
|
||||
left.fill_value, right_sp_values,
|
||||
right.sp_index, right.fill_value)
|
||||
|
||||
if result_dtype is None:
|
||||
result_dtype = result.dtype
|
||||
|
||||
return _wrap_result(name, result, index, fill, dtype=result_dtype)
|
||||
|
||||
|
||||
def _wrap_result(name, data, sparse_index, fill_value, dtype=None):
|
||||
""" wrap op result to have correct dtype """
|
||||
if name.startswith('__'):
|
||||
# e.g. __eq__ --> eq
|
||||
name = name[2:-2]
|
||||
|
||||
if name in ('eq', 'ne', 'lt', 'gt', 'le', 'ge'):
|
||||
dtype = np.bool
|
||||
|
||||
if is_bool_dtype(dtype):
|
||||
# fill_value may be np.bool_
|
||||
fill_value = bool(fill_value)
|
||||
return SparseArray(data, sparse_index=sparse_index,
|
||||
fill_value=fill_value, dtype=dtype)
|
||||
|
||||
|
||||
class SparseArray(PandasObject, np.ndarray):
|
||||
"""Data structure for labeled, sparse floating point 1-D data
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : {array-like (1-D), Series, SparseSeries, dict}
|
||||
kind : {'block', 'integer'}
|
||||
fill_value : float
|
||||
Code for missing value. Defaults depends on dtype.
|
||||
0 for int dtype, False for bool dtype, and NaN for other dtypes
|
||||
sparse_index : {BlockIndex, IntIndex}, optional
|
||||
Only if you have one. Mainly used internally
|
||||
|
||||
Notes
|
||||
-----
|
||||
SparseArray objects are immutable via the typical Python means. If you
|
||||
must change values, convert to dense, make your changes, then convert back
|
||||
to sparse
|
||||
"""
|
||||
__array_priority__ = 15
|
||||
_typ = 'array'
|
||||
_subtyp = 'sparse_array'
|
||||
|
||||
sp_index = None
|
||||
fill_value = None
|
||||
|
||||
def __new__(cls, data, sparse_index=None, index=None, kind='integer',
|
||||
fill_value=None, dtype=None, copy=False):
|
||||
|
||||
if index is not None:
|
||||
if data is None:
|
||||
data = np.nan
|
||||
if not is_scalar(data):
|
||||
raise Exception("must only pass scalars with an index ")
|
||||
dtype = infer_dtype_from_scalar(data)[0]
|
||||
data = construct_1d_arraylike_from_scalar(
|
||||
data, len(index), dtype)
|
||||
|
||||
if isinstance(data, ABCSparseSeries):
|
||||
data = data.values
|
||||
is_sparse_array = isinstance(data, SparseArray)
|
||||
|
||||
if dtype is not None:
|
||||
dtype = np.dtype(dtype)
|
||||
|
||||
if is_sparse_array:
|
||||
sparse_index = data.sp_index
|
||||
values = data.sp_values
|
||||
fill_value = data.fill_value
|
||||
else:
|
||||
# array-like
|
||||
if sparse_index is None:
|
||||
if dtype is not None:
|
||||
data = np.asarray(data, dtype=dtype)
|
||||
res = make_sparse(data, kind=kind, fill_value=fill_value)
|
||||
values, sparse_index, fill_value = res
|
||||
else:
|
||||
values = _sanitize_values(data)
|
||||
if len(values) != sparse_index.npoints:
|
||||
raise AssertionError("Non array-like type {type} must "
|
||||
"have the same length as the index"
|
||||
.format(type=type(values)))
|
||||
# Create array, do *not* copy data by default
|
||||
if copy:
|
||||
subarr = np.array(values, dtype=dtype, copy=True)
|
||||
else:
|
||||
subarr = np.asarray(values, dtype=dtype)
|
||||
# Change the class of the array to be the subclass type.
|
||||
return cls._simple_new(subarr, sparse_index, fill_value)
|
||||
|
||||
@classmethod
|
||||
def _simple_new(cls, data, sp_index, fill_value):
|
||||
if not isinstance(sp_index, SparseIndex):
|
||||
# caller must pass SparseIndex
|
||||
raise ValueError('sp_index must be a SparseIndex')
|
||||
|
||||
if fill_value is None:
|
||||
if sp_index.ngaps > 0:
|
||||
# has missing hole
|
||||
fill_value = np.nan
|
||||
else:
|
||||
fill_value = na_value_for_dtype(data.dtype)
|
||||
|
||||
if (is_integer_dtype(data) and is_float(fill_value) and
|
||||
sp_index.ngaps > 0):
|
||||
# if float fill_value is being included in dense repr,
|
||||
# convert values to float
|
||||
data = data.astype(float)
|
||||
|
||||
result = data.view(cls)
|
||||
|
||||
if not isinstance(sp_index, SparseIndex):
|
||||
# caller must pass SparseIndex
|
||||
raise ValueError('sp_index must be a SparseIndex')
|
||||
|
||||
result.sp_index = sp_index
|
||||
result._fill_value = fill_value
|
||||
return result
|
||||
|
||||
@property
|
||||
def _constructor(self):
|
||||
return lambda x: SparseArray(x, fill_value=self.fill_value,
|
||||
kind=self.kind)
|
||||
|
||||
@property
|
||||
def kind(self):
|
||||
if isinstance(self.sp_index, BlockIndex):
|
||||
return 'block'
|
||||
elif isinstance(self.sp_index, IntIndex):
|
||||
return 'integer'
|
||||
|
||||
@Appender(IndexOpsMixin.memory_usage.__doc__)
|
||||
def memory_usage(self, deep=False):
|
||||
values = self.sp_values
|
||||
|
||||
v = values.nbytes
|
||||
|
||||
if deep and is_object_dtype(self) and not PYPY:
|
||||
v += lib.memory_usage_of_objects(values)
|
||||
|
||||
return v
|
||||
|
||||
def __array_wrap__(self, out_arr, context=None):
|
||||
"""
|
||||
NumPy calls this method when ufunc is applied
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
out_arr : ndarray
|
||||
ufunc result (note that ufunc is only applied to sp_values)
|
||||
context : tuple of 3 elements (ufunc, signature, domain)
|
||||
for example, following is a context when np.sin is applied to
|
||||
SparseArray,
|
||||
|
||||
(<ufunc 'sin'>, (SparseArray,), 0))
|
||||
|
||||
See http://docs.scipy.org/doc/numpy/user/basics.subclassing.html
|
||||
"""
|
||||
if isinstance(context, tuple) and len(context) == 3:
|
||||
ufunc, args, domain = context
|
||||
# to apply ufunc only to fill_value (to avoid recursive call)
|
||||
args = [getattr(a, 'fill_value', a) for a in args]
|
||||
with np.errstate(all='ignore'):
|
||||
fill_value = ufunc(self.fill_value, *args[1:])
|
||||
else:
|
||||
fill_value = self.fill_value
|
||||
|
||||
return self._simple_new(out_arr, sp_index=self.sp_index,
|
||||
fill_value=fill_value)
|
||||
|
||||
def __array_finalize__(self, obj):
|
||||
"""
|
||||
Gets called after any ufunc or other array operations, necessary
|
||||
to pass on the index.
|
||||
"""
|
||||
self.sp_index = getattr(obj, 'sp_index', None)
|
||||
self._fill_value = getattr(obj, 'fill_value', None)
|
||||
|
||||
def __reduce__(self):
|
||||
"""Necessary for making this object picklable"""
|
||||
object_state = list(np.ndarray.__reduce__(self))
|
||||
subclass_state = self.fill_value, self.sp_index
|
||||
object_state[2] = self.sp_values.__reduce__()[2]
|
||||
object_state[2] = (object_state[2], subclass_state)
|
||||
return tuple(object_state)
|
||||
|
||||
def __setstate__(self, state):
|
||||
"""Necessary for making this object picklable"""
|
||||
nd_state, own_state = state
|
||||
np.ndarray.__setstate__(self, nd_state)
|
||||
|
||||
fill_value, sp_index = own_state[:2]
|
||||
self.sp_index = sp_index
|
||||
self._fill_value = fill_value
|
||||
|
||||
def __len__(self):
|
||||
try:
|
||||
return self.sp_index.length
|
||||
except:
|
||||
return 0
|
||||
|
||||
def __unicode__(self):
|
||||
return '{self}\nFill: {fill}\n{index}'.format(
|
||||
self=printing.pprint_thing(self),
|
||||
fill=printing.pprint_thing(self.fill_value),
|
||||
index=printing.pprint_thing(self.sp_index))
|
||||
|
||||
def disable(self, other):
|
||||
raise NotImplementedError('inplace binary ops not supported')
|
||||
# Inplace operators
|
||||
__iadd__ = disable
|
||||
__isub__ = disable
|
||||
__imul__ = disable
|
||||
__itruediv__ = disable
|
||||
__ifloordiv__ = disable
|
||||
__ipow__ = disable
|
||||
|
||||
# Python 2 division operators
|
||||
if not compat.PY3:
|
||||
__idiv__ = disable
|
||||
|
||||
@property
|
||||
def values(self):
|
||||
"""
|
||||
Dense values
|
||||
"""
|
||||
output = np.empty(len(self), dtype=self.dtype)
|
||||
int_index = self.sp_index.to_int_index()
|
||||
output.fill(self.fill_value)
|
||||
output.put(int_index.indices, self)
|
||||
return output
|
||||
|
||||
@property
|
||||
def shape(self):
|
||||
return (len(self),)
|
||||
|
||||
@property
|
||||
def sp_values(self):
|
||||
# caching not an option, leaks memory
|
||||
return self.view(np.ndarray)
|
||||
|
||||
@property
|
||||
def fill_value(self):
|
||||
return self._fill_value
|
||||
|
||||
@fill_value.setter
|
||||
def fill_value(self, value):
|
||||
if not is_scalar(value):
|
||||
raise ValueError('fill_value must be a scalar')
|
||||
# if the specified value triggers type promotion, raise ValueError
|
||||
new_dtype, fill_value = maybe_promote(self.dtype, value)
|
||||
if is_dtype_equal(self.dtype, new_dtype):
|
||||
self._fill_value = fill_value
|
||||
else:
|
||||
msg = 'unable to set fill_value {fill} to {dtype} dtype'
|
||||
raise ValueError(msg.format(fill=value, dtype=self.dtype))
|
||||
|
||||
def get_values(self, fill=None):
|
||||
""" return a dense representation """
|
||||
return self.to_dense(fill=fill)
|
||||
|
||||
def to_dense(self, fill=None):
|
||||
"""
|
||||
Convert SparseArray to a NumPy array.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fill: float, default None
|
||||
.. deprecated:: 0.20.0
|
||||
This argument is not respected by this function.
|
||||
|
||||
Returns
|
||||
-------
|
||||
arr : NumPy array
|
||||
"""
|
||||
if fill is not None:
|
||||
warnings.warn(("The 'fill' parameter has been deprecated and "
|
||||
"will be removed in a future version."),
|
||||
FutureWarning, stacklevel=2)
|
||||
return self.values
|
||||
|
||||
def __iter__(self):
|
||||
if np.issubdtype(self.dtype, np.floating):
|
||||
boxer = float
|
||||
elif np.issubdtype(self.dtype, np.integer):
|
||||
boxer = int
|
||||
else:
|
||||
boxer = lambda x: x
|
||||
|
||||
for i in range(len(self)):
|
||||
r = self._get_val_at(i)
|
||||
|
||||
# box em
|
||||
yield boxer(r)
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""
|
||||
|
||||
"""
|
||||
|
||||
if is_integer(key):
|
||||
return self._get_val_at(key)
|
||||
elif isinstance(key, tuple):
|
||||
data_slice = self.values[key]
|
||||
else:
|
||||
if isinstance(key, SparseArray):
|
||||
if is_bool_dtype(key):
|
||||
key = key.to_dense()
|
||||
else:
|
||||
key = np.asarray(key)
|
||||
|
||||
if hasattr(key, '__len__') and len(self) != len(key):
|
||||
return self.take(key)
|
||||
else:
|
||||
data_slice = self.values[key]
|
||||
|
||||
return self._constructor(data_slice)
|
||||
|
||||
def __getslice__(self, i, j):
|
||||
if i < 0:
|
||||
i = 0
|
||||
if j < 0:
|
||||
j = 0
|
||||
slobj = slice(i, j)
|
||||
return self.__getitem__(slobj)
|
||||
|
||||
def _get_val_at(self, loc):
|
||||
n = len(self)
|
||||
if loc < 0:
|
||||
loc += n
|
||||
|
||||
if loc >= n or loc < 0:
|
||||
raise IndexError('Out of bounds access')
|
||||
|
||||
sp_loc = self.sp_index.lookup(loc)
|
||||
if sp_loc == -1:
|
||||
return self.fill_value
|
||||
else:
|
||||
return libindex.get_value_at(self, sp_loc)
|
||||
|
||||
@Appender(_index_shared_docs['take'] % _sparray_doc_kwargs)
|
||||
def take(self, indices, axis=0, allow_fill=True,
|
||||
fill_value=None, **kwargs):
|
||||
"""
|
||||
Sparse-compatible version of ndarray.take
|
||||
|
||||
Returns
|
||||
-------
|
||||
taken : ndarray
|
||||
"""
|
||||
nv.validate_take(tuple(), kwargs)
|
||||
|
||||
if axis:
|
||||
raise ValueError("axis must be 0, input was {axis}"
|
||||
.format(axis=axis))
|
||||
|
||||
if is_integer(indices):
|
||||
# return scalar
|
||||
return self[indices]
|
||||
|
||||
indices = _ensure_platform_int(indices)
|
||||
n = len(self)
|
||||
if allow_fill and fill_value is not None:
|
||||
# allow -1 to indicate self.fill_value,
|
||||
# self.fill_value may not be NaN
|
||||
if (indices < -1).any():
|
||||
msg = ('When allow_fill=True and fill_value is not None, '
|
||||
'all indices must be >= -1')
|
||||
raise ValueError(msg)
|
||||
elif (n <= indices).any():
|
||||
msg = 'index is out of bounds for size {size}'.format(size=n)
|
||||
raise IndexError(msg)
|
||||
else:
|
||||
if ((indices < -n) | (n <= indices)).any():
|
||||
msg = 'index is out of bounds for size {size}'.format(size=n)
|
||||
raise IndexError(msg)
|
||||
|
||||
indices = indices.astype(np.int32)
|
||||
if not (allow_fill and fill_value is not None):
|
||||
indices = indices.copy()
|
||||
indices[indices < 0] += n
|
||||
|
||||
locs = self.sp_index.lookup_array(indices)
|
||||
indexer = np.arange(len(locs), dtype=np.int32)
|
||||
mask = locs != -1
|
||||
if mask.any():
|
||||
indexer = indexer[mask]
|
||||
new_values = self.sp_values.take(locs[mask])
|
||||
else:
|
||||
indexer = np.empty(shape=(0, ), dtype=np.int32)
|
||||
new_values = np.empty(shape=(0, ), dtype=self.sp_values.dtype)
|
||||
|
||||
sp_index = _make_index(len(indices), indexer, kind=self.sp_index)
|
||||
return self._simple_new(new_values, sp_index, self.fill_value)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
# if is_integer(key):
|
||||
# self.values[key] = value
|
||||
# else:
|
||||
# raise Exception("SparseArray does not support setting non-scalars
|
||||
# via setitem")
|
||||
raise TypeError(
|
||||
"SparseArray does not support item assignment via setitem")
|
||||
|
||||
def __setslice__(self, i, j, value):
|
||||
if i < 0:
|
||||
i = 0
|
||||
if j < 0:
|
||||
j = 0
|
||||
slobj = slice(i, j) # noqa
|
||||
|
||||
# if not is_scalar(value):
|
||||
# raise Exception("SparseArray does not support setting non-scalars
|
||||
# via slices")
|
||||
|
||||
# x = self.values
|
||||
# x[slobj] = value
|
||||
# self.values = x
|
||||
raise TypeError("SparseArray does not support item assignment via "
|
||||
"slices")
|
||||
|
||||
def astype(self, dtype=None, copy=True):
|
||||
dtype = np.dtype(dtype)
|
||||
sp_values = astype_nansafe(self.sp_values, dtype, copy=copy)
|
||||
try:
|
||||
if is_bool_dtype(dtype):
|
||||
# to avoid np.bool_ dtype
|
||||
fill_value = bool(self.fill_value)
|
||||
else:
|
||||
fill_value = dtype.type(self.fill_value)
|
||||
except ValueError:
|
||||
msg = 'unable to coerce current fill_value {fill} to {dtype} dtype'
|
||||
raise ValueError(msg.format(fill=self.fill_value, dtype=dtype))
|
||||
return self._simple_new(sp_values, self.sp_index,
|
||||
fill_value=fill_value)
|
||||
|
||||
def copy(self, deep=True):
|
||||
"""
|
||||
Make a copy of the SparseArray. Only the actual sparse values need to
|
||||
be copied.
|
||||
"""
|
||||
if deep:
|
||||
values = self.sp_values.copy()
|
||||
else:
|
||||
values = self.sp_values
|
||||
return SparseArray(values, sparse_index=self.sp_index,
|
||||
dtype=self.dtype, fill_value=self.fill_value)
|
||||
|
||||
def count(self):
|
||||
"""
|
||||
Compute sum of non-NA/null observations in SparseArray. If the
|
||||
fill_value is not NaN, the "sparse" locations will be included in the
|
||||
observation count.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nobs : int
|
||||
"""
|
||||
sp_values = self.sp_values
|
||||
valid_spvals = np.isfinite(sp_values).sum()
|
||||
if self._null_fill_value:
|
||||
return valid_spvals
|
||||
else:
|
||||
return valid_spvals + self.sp_index.ngaps
|
||||
|
||||
@property
|
||||
def _null_fill_value(self):
|
||||
return isna(self.fill_value)
|
||||
|
||||
@property
|
||||
def _valid_sp_values(self):
|
||||
sp_vals = self.sp_values
|
||||
mask = notna(sp_vals)
|
||||
return sp_vals[mask]
|
||||
|
||||
@Appender(_index_shared_docs['fillna'] % _sparray_doc_kwargs)
|
||||
def fillna(self, value, downcast=None):
|
||||
if downcast is not None:
|
||||
raise NotImplementedError
|
||||
|
||||
if issubclass(self.dtype.type, np.floating):
|
||||
value = float(value)
|
||||
|
||||
new_values = np.where(isna(self.sp_values), value, self.sp_values)
|
||||
fill_value = value if self._null_fill_value else self.fill_value
|
||||
|
||||
return self._simple_new(new_values, self.sp_index,
|
||||
fill_value=fill_value)
|
||||
|
||||
def all(self, axis=0, *args, **kwargs):
|
||||
"""
|
||||
Tests whether all elements evaluate True
|
||||
|
||||
Returns
|
||||
-------
|
||||
all : bool
|
||||
|
||||
See Also
|
||||
--------
|
||||
numpy.all
|
||||
"""
|
||||
nv.validate_all(args, kwargs)
|
||||
|
||||
values = self.sp_values
|
||||
|
||||
if len(values) != len(self) and not np.all(self.fill_value):
|
||||
return False
|
||||
|
||||
return values.all()
|
||||
|
||||
def any(self, axis=0, *args, **kwargs):
|
||||
"""
|
||||
Tests whether at least one of elements evaluate True
|
||||
|
||||
Returns
|
||||
-------
|
||||
any : bool
|
||||
|
||||
See Also
|
||||
--------
|
||||
numpy.any
|
||||
"""
|
||||
nv.validate_any(args, kwargs)
|
||||
|
||||
values = self.sp_values
|
||||
|
||||
if len(values) != len(self) and np.any(self.fill_value):
|
||||
return True
|
||||
|
||||
return values.any()
|
||||
|
||||
def sum(self, axis=0, *args, **kwargs):
|
||||
"""
|
||||
Sum of non-NA/null values
|
||||
|
||||
Returns
|
||||
-------
|
||||
sum : float
|
||||
"""
|
||||
nv.validate_sum(args, kwargs)
|
||||
valid_vals = self._valid_sp_values
|
||||
sp_sum = valid_vals.sum()
|
||||
if self._null_fill_value:
|
||||
return sp_sum
|
||||
else:
|
||||
nsparse = self.sp_index.ngaps
|
||||
return sp_sum + self.fill_value * nsparse
|
||||
|
||||
def cumsum(self, axis=0, *args, **kwargs):
|
||||
"""
|
||||
Cumulative sum of non-NA/null values.
|
||||
|
||||
When performing the cumulative summation, any non-NA/null values will
|
||||
be skipped. The resulting SparseArray will preserve the locations of
|
||||
NaN values, but the fill value will be `np.nan` regardless.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
axis : int or None
|
||||
Axis over which to perform the cumulative summation. If None,
|
||||
perform cumulative summation over flattened array.
|
||||
|
||||
Returns
|
||||
-------
|
||||
cumsum : SparseArray
|
||||
"""
|
||||
nv.validate_cumsum(args, kwargs)
|
||||
|
||||
if axis is not None and axis >= self.ndim: # Mimic ndarray behaviour.
|
||||
raise ValueError("axis(={axis}) out of bounds".format(axis=axis))
|
||||
|
||||
if not self._null_fill_value:
|
||||
return SparseArray(self.to_dense()).cumsum()
|
||||
|
||||
return SparseArray(self.sp_values.cumsum(), sparse_index=self.sp_index,
|
||||
fill_value=self.fill_value)
|
||||
|
||||
def mean(self, axis=0, *args, **kwargs):
|
||||
"""
|
||||
Mean of non-NA/null values
|
||||
|
||||
Returns
|
||||
-------
|
||||
mean : float
|
||||
"""
|
||||
nv.validate_mean(args, kwargs)
|
||||
valid_vals = self._valid_sp_values
|
||||
sp_sum = valid_vals.sum()
|
||||
ct = len(valid_vals)
|
||||
|
||||
if self._null_fill_value:
|
||||
return sp_sum / ct
|
||||
else:
|
||||
nsparse = self.sp_index.ngaps
|
||||
return (sp_sum + self.fill_value * nsparse) / (ct + nsparse)
|
||||
|
||||
def value_counts(self, dropna=True):
|
||||
"""
|
||||
Returns a Series containing counts of unique values.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dropna : boolean, default True
|
||||
Don't include counts of NaN, even if NaN is in sp_values.
|
||||
|
||||
Returns
|
||||
-------
|
||||
counts : Series
|
||||
"""
|
||||
keys, counts = algos._value_counts_arraylike(self.sp_values,
|
||||
dropna=dropna)
|
||||
fcounts = self.sp_index.ngaps
|
||||
if fcounts > 0:
|
||||
if self._null_fill_value and dropna:
|
||||
pass
|
||||
else:
|
||||
if self._null_fill_value:
|
||||
mask = pd.isna(keys)
|
||||
else:
|
||||
mask = keys == self.fill_value
|
||||
|
||||
if mask.any():
|
||||
counts[mask] += fcounts
|
||||
else:
|
||||
keys = np.insert(keys, 0, self.fill_value)
|
||||
counts = np.insert(counts, 0, fcounts)
|
||||
|
||||
if not isinstance(keys, pd.Index):
|
||||
keys = pd.Index(keys)
|
||||
result = pd.Series(counts, index=keys)
|
||||
return result
|
||||
|
||||
|
||||
def _maybe_to_dense(obj):
|
||||
""" try to convert to dense """
|
||||
if hasattr(obj, 'to_dense'):
|
||||
return obj.to_dense()
|
||||
return obj
|
||||
|
||||
|
||||
def _maybe_to_sparse(array):
|
||||
""" array must be SparseSeries or SparseArray """
|
||||
if isinstance(array, ABCSparseSeries):
|
||||
array = array.values.copy()
|
||||
return array
|
||||
|
||||
|
||||
def _sanitize_values(arr):
|
||||
"""
|
||||
return an ndarray for our input,
|
||||
in a platform independent manner
|
||||
"""
|
||||
|
||||
if hasattr(arr, 'values'):
|
||||
arr = arr.values
|
||||
else:
|
||||
|
||||
# scalar
|
||||
if is_scalar(arr):
|
||||
arr = [arr]
|
||||
|
||||
# ndarray
|
||||
if isinstance(arr, np.ndarray):
|
||||
pass
|
||||
|
||||
elif is_list_like(arr) and len(arr) > 0:
|
||||
arr = maybe_convert_platform(arr)
|
||||
|
||||
else:
|
||||
arr = np.asarray(arr)
|
||||
|
||||
return arr
|
||||
|
||||
|
||||
def make_sparse(arr, kind='block', fill_value=None):
|
||||
"""
|
||||
Convert ndarray to sparse format
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arr : ndarray
|
||||
kind : {'block', 'integer'}
|
||||
fill_value : NaN or another value
|
||||
|
||||
Returns
|
||||
-------
|
||||
(sparse_values, index) : (ndarray, SparseIndex)
|
||||
"""
|
||||
|
||||
arr = _sanitize_values(arr)
|
||||
|
||||
if arr.ndim > 1:
|
||||
raise TypeError("expected dimension <= 1 data")
|
||||
|
||||
if fill_value is None:
|
||||
fill_value = na_value_for_dtype(arr.dtype)
|
||||
|
||||
if isna(fill_value):
|
||||
mask = notna(arr)
|
||||
else:
|
||||
# For str arrays in NumPy 1.12.0, operator!= below isn't
|
||||
# element-wise but just returns False if fill_value is not str,
|
||||
# so cast to object comparison to be safe
|
||||
if is_string_dtype(arr):
|
||||
arr = arr.astype(object)
|
||||
|
||||
if is_object_dtype(arr.dtype):
|
||||
# element-wise equality check method in numpy doesn't treat
|
||||
# each element type, eg. 0, 0.0, and False are treated as
|
||||
# same. So we have to check the both of its type and value.
|
||||
mask = splib.make_mask_object_ndarray(arr, fill_value)
|
||||
else:
|
||||
mask = arr != fill_value
|
||||
|
||||
length = len(arr)
|
||||
if length != mask.size:
|
||||
# the arr is a SparseArray
|
||||
indices = mask.sp_index.indices
|
||||
else:
|
||||
indices = mask.nonzero()[0].astype(np.int32)
|
||||
|
||||
index = _make_index(length, indices, kind)
|
||||
sparsified_values = arr[mask]
|
||||
return sparsified_values, index, fill_value
|
||||
|
||||
|
||||
def _make_index(length, indices, kind):
|
||||
|
||||
if kind == 'block' or isinstance(kind, BlockIndex):
|
||||
locs, lens = splib.get_blocks(indices)
|
||||
index = BlockIndex(length, locs, lens)
|
||||
elif kind == 'integer' or isinstance(kind, IntIndex):
|
||||
index = IntIndex(length, indices)
|
||||
else: # pragma: no cover
|
||||
raise ValueError('must be block or integer type')
|
||||
return index
|
||||
|
||||
|
||||
ops.add_special_arithmetic_methods(SparseArray)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
Interaction with scipy.sparse matrices.
|
||||
|
||||
Currently only includes SparseSeries.to_coo helpers.
|
||||
"""
|
||||
from pandas.core.index import MultiIndex, Index
|
||||
from pandas.core.series import Series
|
||||
from pandas.compat import OrderedDict, lmap
|
||||
|
||||
|
||||
def _check_is_partition(parts, whole):
|
||||
whole = set(whole)
|
||||
parts = [set(x) for x in parts]
|
||||
if set.intersection(*parts) != set():
|
||||
raise ValueError(
|
||||
'Is not a partition because intersection is not null.')
|
||||
if set.union(*parts) != whole:
|
||||
raise ValueError('Is not a partition because union is not the whole.')
|
||||
|
||||
|
||||
def _to_ijv(ss, row_levels=(0, ), column_levels=(1, ), sort_labels=False):
|
||||
""" For arbitrary (MultiIndexed) SparseSeries return
|
||||
(v, i, j, ilabels, jlabels) where (v, (i, j)) is suitable for
|
||||
passing to scipy.sparse.coo constructor. """
|
||||
# index and column levels must be a partition of the index
|
||||
_check_is_partition([row_levels, column_levels], range(ss.index.nlevels))
|
||||
|
||||
# from the SparseSeries: get the labels and data for non-null entries
|
||||
values = ss._data.internal_values()._valid_sp_values
|
||||
|
||||
nonnull_labels = ss.dropna()
|
||||
|
||||
def get_indexers(levels):
|
||||
""" Return sparse coords and dense labels for subset levels """
|
||||
|
||||
# TODO: how to do this better? cleanly slice nonnull_labels given the
|
||||
# coord
|
||||
values_ilabels = [tuple(x[i] for i in levels)
|
||||
for x in nonnull_labels.index]
|
||||
if len(levels) == 1:
|
||||
values_ilabels = [x[0] for x in values_ilabels]
|
||||
|
||||
# # performance issues with groupby ###################################
|
||||
# TODO: these two lines can rejplace the code below but
|
||||
# groupby is too slow (in some cases at least)
|
||||
# labels_to_i = ss.groupby(level=levels, sort=sort_labels).first()
|
||||
# labels_to_i[:] = np.arange(labels_to_i.shape[0])
|
||||
|
||||
def _get_label_to_i_dict(labels, sort_labels=False):
|
||||
""" Return OrderedDict of unique labels to number.
|
||||
Optionally sort by label.
|
||||
"""
|
||||
labels = Index(lmap(tuple, labels)).unique().tolist() # squish
|
||||
if sort_labels:
|
||||
labels = sorted(list(labels))
|
||||
d = OrderedDict((k, i) for i, k in enumerate(labels))
|
||||
return (d)
|
||||
|
||||
def _get_index_subset_to_coord_dict(index, subset, sort_labels=False):
|
||||
def robust_get_level_values(i):
|
||||
# if index has labels (that are not None) use those,
|
||||
# else use the level location
|
||||
try:
|
||||
return index.get_level_values(index.names[i])
|
||||
except KeyError:
|
||||
return index.get_level_values(i)
|
||||
|
||||
ilabels = list(zip(*[robust_get_level_values(i) for i in subset]))
|
||||
labels_to_i = _get_label_to_i_dict(ilabels,
|
||||
sort_labels=sort_labels)
|
||||
labels_to_i = Series(labels_to_i)
|
||||
if len(subset) > 1:
|
||||
labels_to_i.index = MultiIndex.from_tuples(labels_to_i.index)
|
||||
labels_to_i.index.names = [index.names[i] for i in subset]
|
||||
else:
|
||||
labels_to_i.index = Index(x[0] for x in labels_to_i.index)
|
||||
labels_to_i.index.name = index.names[subset[0]]
|
||||
|
||||
labels_to_i.name = 'value'
|
||||
return (labels_to_i)
|
||||
|
||||
labels_to_i = _get_index_subset_to_coord_dict(ss.index, levels,
|
||||
sort_labels=sort_labels)
|
||||
# #####################################################################
|
||||
# #####################################################################
|
||||
|
||||
i_coord = labels_to_i[values_ilabels].tolist()
|
||||
i_labels = labels_to_i.index.tolist()
|
||||
|
||||
return i_coord, i_labels
|
||||
|
||||
i_coord, i_labels = get_indexers(row_levels)
|
||||
j_coord, j_labels = get_indexers(column_levels)
|
||||
|
||||
return values, i_coord, j_coord, i_labels, j_labels
|
||||
|
||||
|
||||
def _sparse_series_to_coo(ss, row_levels=(0, ), column_levels=(1, ),
|
||||
sort_labels=False):
|
||||
""" Convert a SparseSeries to a scipy.sparse.coo_matrix using index
|
||||
levels row_levels, column_levels as the row and column
|
||||
labels respectively. Returns the sparse_matrix, row and column labels.
|
||||
"""
|
||||
|
||||
import scipy.sparse
|
||||
|
||||
if ss.index.nlevels < 2:
|
||||
raise ValueError('to_coo requires MultiIndex with nlevels > 2')
|
||||
if not ss.index.is_unique:
|
||||
raise ValueError('Duplicate index entries are not allowed in to_coo '
|
||||
'transformation.')
|
||||
|
||||
# to keep things simple, only rely on integer indexing (not labels)
|
||||
row_levels = [ss.index._get_level_number(x) for x in row_levels]
|
||||
column_levels = [ss.index._get_level_number(x) for x in column_levels]
|
||||
|
||||
v, i, j, rows, columns = _to_ijv(ss, row_levels=row_levels,
|
||||
column_levels=column_levels,
|
||||
sort_labels=sort_labels)
|
||||
sparse_matrix = scipy.sparse.coo_matrix(
|
||||
(v, (i, j)), shape=(len(rows), len(columns)))
|
||||
return sparse_matrix, rows, columns
|
||||
|
||||
|
||||
def _coo_to_sparse_series(A, dense_index=False):
|
||||
""" Convert a scipy.sparse.coo_matrix to a SparseSeries.
|
||||
Use the defaults given in the SparseSeries constructor.
|
||||
"""
|
||||
s = Series(A.data, MultiIndex.from_arrays((A.row, A.col)))
|
||||
s = s.sort_index()
|
||||
s = s.to_sparse() # TODO: specify kind?
|
||||
if dense_index:
|
||||
# is there a better constructor method to use here?
|
||||
i = range(A.shape[0])
|
||||
j = range(A.shape[1])
|
||||
ind = MultiIndex.from_product([i, j])
|
||||
s = s.reindex(ind)
|
||||
return s
|
||||
@@ -0,0 +1,814 @@
|
||||
"""
|
||||
Data structures for sparse float data. Life is made simpler by dealing only
|
||||
with float64 data
|
||||
"""
|
||||
|
||||
# pylint: disable=E1101,E1103,W0231
|
||||
|
||||
import numpy as np
|
||||
import warnings
|
||||
|
||||
from pandas.core.dtypes.missing import isna, notna
|
||||
|
||||
from pandas.compat.numpy import function as nv
|
||||
from pandas.core.index import Index, _ensure_index, InvalidIndexError
|
||||
from pandas.core.series import Series
|
||||
from pandas.core.internals import SingleBlockManager
|
||||
from pandas.core import generic
|
||||
import pandas.core.common as com
|
||||
import pandas.core.ops as ops
|
||||
import pandas._libs.index as libindex
|
||||
from pandas.util._decorators import Appender
|
||||
|
||||
from pandas.core.sparse.array import (
|
||||
make_sparse, SparseArray,
|
||||
_make_index)
|
||||
from pandas._libs.sparse import BlockIndex, IntIndex
|
||||
import pandas._libs.sparse as splib
|
||||
|
||||
from pandas.core.sparse.scipy_sparse import (
|
||||
_sparse_series_to_coo,
|
||||
_coo_to_sparse_series)
|
||||
|
||||
|
||||
_shared_doc_kwargs = dict(axes='index', klass='SparseSeries',
|
||||
axes_single_arg="{0, 'index'}",
|
||||
optional_labels='', optional_axis='')
|
||||
|
||||
|
||||
class SparseSeries(Series):
|
||||
"""Data structure for labeled, sparse floating point data
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : {array-like, Series, SparseSeries, dict}
|
||||
.. versionchanged :: 0.23.0
|
||||
If data is a dict, argument order is maintained for Python 3.6
|
||||
and later.
|
||||
|
||||
kind : {'block', 'integer'}
|
||||
fill_value : float
|
||||
Code for missing value. Defaults depends on dtype.
|
||||
0 for int dtype, False for bool dtype, and NaN for other dtypes
|
||||
sparse_index : {BlockIndex, IntIndex}, optional
|
||||
Only if you have one. Mainly used internally
|
||||
|
||||
Notes
|
||||
-----
|
||||
SparseSeries objects are immutable via the typical Python means. If you
|
||||
must change values, convert to dense, make your changes, then convert back
|
||||
to sparse
|
||||
"""
|
||||
_subtyp = 'sparse_series'
|
||||
|
||||
def __init__(self, data=None, index=None, sparse_index=None, kind='block',
|
||||
fill_value=None, name=None, dtype=None, copy=False,
|
||||
fastpath=False):
|
||||
|
||||
# we are called internally, so short-circuit
|
||||
if fastpath:
|
||||
|
||||
# data is an ndarray, index is defined
|
||||
|
||||
if not isinstance(data, SingleBlockManager):
|
||||
data = SingleBlockManager(data, index, fastpath=True)
|
||||
if copy:
|
||||
data = data.copy()
|
||||
|
||||
else:
|
||||
|
||||
if data is None:
|
||||
data = []
|
||||
|
||||
if isinstance(data, Series) and name is None:
|
||||
name = data.name
|
||||
|
||||
if isinstance(data, SparseArray):
|
||||
if index is not None:
|
||||
assert (len(index) == len(data))
|
||||
sparse_index = data.sp_index
|
||||
if fill_value is None:
|
||||
fill_value = data.fill_value
|
||||
|
||||
data = np.asarray(data)
|
||||
|
||||
elif isinstance(data, SparseSeries):
|
||||
if index is None:
|
||||
index = data.index.view()
|
||||
if fill_value is None:
|
||||
fill_value = data.fill_value
|
||||
# extract the SingleBlockManager
|
||||
data = data._data
|
||||
|
||||
elif isinstance(data, (Series, dict)):
|
||||
data = Series(data, index=index)
|
||||
index = data.index.view()
|
||||
|
||||
res = make_sparse(data, kind=kind, fill_value=fill_value)
|
||||
data, sparse_index, fill_value = res
|
||||
|
||||
elif isinstance(data, (tuple, list, np.ndarray)):
|
||||
# array-like
|
||||
if sparse_index is None:
|
||||
res = make_sparse(data, kind=kind, fill_value=fill_value)
|
||||
data, sparse_index, fill_value = res
|
||||
else:
|
||||
assert (len(data) == sparse_index.npoints)
|
||||
|
||||
elif isinstance(data, SingleBlockManager):
|
||||
if dtype is not None:
|
||||
data = data.astype(dtype)
|
||||
if index is None:
|
||||
index = data.index.view()
|
||||
elif not data.index.equals(index) or copy: # pragma: no cover
|
||||
# GH#19275 SingleBlockManager input should only be called
|
||||
# internally
|
||||
raise AssertionError('Cannot pass both SingleBlockManager '
|
||||
'`data` argument and a different '
|
||||
'`index` argument. `copy` must '
|
||||
'be False.')
|
||||
|
||||
else:
|
||||
length = len(index)
|
||||
|
||||
if data == fill_value or (isna(data) and isna(fill_value)):
|
||||
if kind == 'block':
|
||||
sparse_index = BlockIndex(length, [], [])
|
||||
else:
|
||||
sparse_index = IntIndex(length, [])
|
||||
data = np.array([])
|
||||
|
||||
else:
|
||||
if kind == 'block':
|
||||
locs, lens = ([0], [length]) if length else ([], [])
|
||||
sparse_index = BlockIndex(length, locs, lens)
|
||||
else:
|
||||
sparse_index = IntIndex(length, index)
|
||||
v = data
|
||||
data = np.empty(length)
|
||||
data.fill(v)
|
||||
|
||||
if index is None:
|
||||
index = com._default_index(sparse_index.length)
|
||||
index = _ensure_index(index)
|
||||
|
||||
# create/copy the manager
|
||||
if isinstance(data, SingleBlockManager):
|
||||
|
||||
if copy:
|
||||
data = data.copy()
|
||||
else:
|
||||
|
||||
# create a sparse array
|
||||
if not isinstance(data, SparseArray):
|
||||
data = SparseArray(data, sparse_index=sparse_index,
|
||||
fill_value=fill_value, dtype=dtype,
|
||||
copy=copy)
|
||||
|
||||
data = SingleBlockManager(data, index)
|
||||
|
||||
generic.NDFrame.__init__(self, data)
|
||||
|
||||
self.index = index
|
||||
self.name = name
|
||||
|
||||
@property
|
||||
def values(self):
|
||||
""" return the array """
|
||||
return self.block.values
|
||||
|
||||
def __array__(self, result=None):
|
||||
""" the array interface, return my values """
|
||||
return self.block.values
|
||||
|
||||
def get_values(self):
|
||||
""" same as values """
|
||||
return self.block.to_dense().view()
|
||||
|
||||
@property
|
||||
def block(self):
|
||||
return self._data._block
|
||||
|
||||
@property
|
||||
def fill_value(self):
|
||||
return self.block.fill_value
|
||||
|
||||
@fill_value.setter
|
||||
def fill_value(self, v):
|
||||
self.block.fill_value = v
|
||||
|
||||
@property
|
||||
def sp_index(self):
|
||||
return self.block.sp_index
|
||||
|
||||
@property
|
||||
def sp_values(self):
|
||||
return self.values.sp_values
|
||||
|
||||
@property
|
||||
def npoints(self):
|
||||
return self.sp_index.npoints
|
||||
|
||||
@classmethod
|
||||
def from_array(cls, arr, index=None, name=None, copy=False,
|
||||
fill_value=None, fastpath=False):
|
||||
"""Construct SparseSeries from array.
|
||||
|
||||
.. deprecated:: 0.23.0
|
||||
Use the pd.SparseSeries(..) constructor instead.
|
||||
"""
|
||||
warnings.warn("'from_array' is deprecated and will be removed in a "
|
||||
"future version. Please use the pd.SparseSeries(..) "
|
||||
"constructor instead.", FutureWarning, stacklevel=2)
|
||||
return cls(arr, index=index, name=name, copy=copy,
|
||||
fill_value=fill_value, fastpath=fastpath)
|
||||
|
||||
@property
|
||||
def _constructor(self):
|
||||
return SparseSeries
|
||||
|
||||
@property
|
||||
def _constructor_expanddim(self):
|
||||
from pandas.core.sparse.api import SparseDataFrame
|
||||
return SparseDataFrame
|
||||
|
||||
@property
|
||||
def kind(self):
|
||||
if isinstance(self.sp_index, BlockIndex):
|
||||
return 'block'
|
||||
elif isinstance(self.sp_index, IntIndex):
|
||||
return 'integer'
|
||||
|
||||
def as_sparse_array(self, kind=None, fill_value=None, copy=False):
|
||||
""" return my self as a sparse array, do not copy by default """
|
||||
|
||||
if fill_value is None:
|
||||
fill_value = self.fill_value
|
||||
if kind is None:
|
||||
kind = self.kind
|
||||
return SparseArray(self.values, sparse_index=self.sp_index,
|
||||
fill_value=fill_value, kind=kind, copy=copy)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.block)
|
||||
|
||||
@property
|
||||
def shape(self):
|
||||
return self._data.shape
|
||||
|
||||
def __unicode__(self):
|
||||
# currently, unicode is same as repr...fixes infinite loop
|
||||
series_rep = Series.__unicode__(self)
|
||||
rep = '{series}\n{index!r}'.format(series=series_rep,
|
||||
index=self.sp_index)
|
||||
return rep
|
||||
|
||||
def __array_wrap__(self, result, context=None):
|
||||
"""
|
||||
Gets called prior to a ufunc (and after)
|
||||
|
||||
See SparseArray.__array_wrap__ for detail.
|
||||
"""
|
||||
if isinstance(context, tuple) and len(context) == 3:
|
||||
ufunc, args, domain = context
|
||||
args = [getattr(a, 'fill_value', a) for a in args]
|
||||
with np.errstate(all='ignore'):
|
||||
fill_value = ufunc(self.fill_value, *args[1:])
|
||||
else:
|
||||
fill_value = self.fill_value
|
||||
|
||||
return self._constructor(result, index=self.index,
|
||||
sparse_index=self.sp_index,
|
||||
fill_value=fill_value,
|
||||
copy=False).__finalize__(self)
|
||||
|
||||
def __array_finalize__(self, obj):
|
||||
"""
|
||||
Gets called after any ufunc or other array operations, necessary
|
||||
to pass on the index.
|
||||
"""
|
||||
self.name = getattr(obj, 'name', None)
|
||||
self.fill_value = getattr(obj, 'fill_value', None)
|
||||
|
||||
def _reduce(self, op, name, axis=0, skipna=True, numeric_only=None,
|
||||
filter_type=None, **kwds):
|
||||
""" perform a reduction operation """
|
||||
return op(self.get_values(), skipna=skipna, **kwds)
|
||||
|
||||
def __getstate__(self):
|
||||
# pickling
|
||||
return dict(_typ=self._typ, _subtyp=self._subtyp, _data=self._data,
|
||||
fill_value=self.fill_value, name=self.name)
|
||||
|
||||
def _unpickle_series_compat(self, state):
|
||||
|
||||
nd_state, own_state = state
|
||||
|
||||
# recreate the ndarray
|
||||
data = np.empty(nd_state[1], dtype=nd_state[2])
|
||||
np.ndarray.__setstate__(data, nd_state)
|
||||
|
||||
index, fill_value, sp_index = own_state[:3]
|
||||
name = None
|
||||
if len(own_state) > 3:
|
||||
name = own_state[3]
|
||||
|
||||
# create a sparse array
|
||||
if not isinstance(data, SparseArray):
|
||||
data = SparseArray(data, sparse_index=sp_index,
|
||||
fill_value=fill_value, copy=False)
|
||||
|
||||
# recreate
|
||||
data = SingleBlockManager(data, index, fastpath=True)
|
||||
generic.NDFrame.__init__(self, data)
|
||||
|
||||
self._set_axis(0, index)
|
||||
self.name = name
|
||||
|
||||
def __iter__(self):
|
||||
""" forward to the array """
|
||||
return iter(self.values)
|
||||
|
||||
def _set_subtyp(self, is_all_dates):
|
||||
if is_all_dates:
|
||||
object.__setattr__(self, '_subtyp', 'sparse_time_series')
|
||||
else:
|
||||
object.__setattr__(self, '_subtyp', 'sparse_series')
|
||||
|
||||
def _ixs(self, i, axis=0):
|
||||
"""
|
||||
Return the i-th value or values in the SparseSeries by location
|
||||
|
||||
Parameters
|
||||
----------
|
||||
i : int, slice, or sequence of integers
|
||||
|
||||
Returns
|
||||
-------
|
||||
value : scalar (int) or Series (slice, sequence)
|
||||
"""
|
||||
label = self.index[i]
|
||||
if isinstance(label, Index):
|
||||
return self.take(i, axis=axis)
|
||||
else:
|
||||
return self._get_val_at(i)
|
||||
|
||||
def _get_val_at(self, loc):
|
||||
""" forward to the array """
|
||||
return self.block.values._get_val_at(loc)
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return self.index.get_value(self, key)
|
||||
|
||||
except InvalidIndexError:
|
||||
pass
|
||||
except KeyError:
|
||||
if isinstance(key, (int, np.integer)):
|
||||
return self._get_val_at(key)
|
||||
elif key is Ellipsis:
|
||||
return self
|
||||
raise Exception('Requested index not in this series!')
|
||||
|
||||
except TypeError:
|
||||
# Could not hash item, must be array-like?
|
||||
pass
|
||||
|
||||
key = com._values_from_object(key)
|
||||
if self.index.nlevels > 1 and isinstance(key, tuple):
|
||||
# to handle MultiIndex labels
|
||||
key = self.index.get_loc(key)
|
||||
return self._constructor(self.values[key],
|
||||
index=self.index[key]).__finalize__(self)
|
||||
|
||||
def _get_values(self, indexer):
|
||||
try:
|
||||
return self._constructor(self._data.get_slice(indexer),
|
||||
fastpath=True).__finalize__(self)
|
||||
except Exception:
|
||||
return self[indexer]
|
||||
|
||||
def _set_with_engine(self, key, value):
|
||||
return self._set_value(key, value)
|
||||
|
||||
def abs(self):
|
||||
"""
|
||||
Return an object with absolute value taken. Only applicable to objects
|
||||
that are all numeric
|
||||
|
||||
Returns
|
||||
-------
|
||||
abs: type of caller
|
||||
"""
|
||||
return self._constructor(np.abs(self.values),
|
||||
index=self.index).__finalize__(self)
|
||||
|
||||
def get(self, label, default=None):
|
||||
"""
|
||||
Returns value occupying requested label, default to specified
|
||||
missing value if not present. Analogous to dict.get
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : object
|
||||
Label value looking for
|
||||
default : object, optional
|
||||
Value to return if label not in index
|
||||
|
||||
Returns
|
||||
-------
|
||||
y : scalar
|
||||
"""
|
||||
if label in self.index:
|
||||
loc = self.index.get_loc(label)
|
||||
return self._get_val_at(loc)
|
||||
else:
|
||||
return default
|
||||
|
||||
def get_value(self, label, takeable=False):
|
||||
"""
|
||||
Retrieve single value at passed index label
|
||||
|
||||
.. deprecated:: 0.21.0
|
||||
|
||||
Please use .at[] or .iat[] accessors.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
index : label
|
||||
takeable : interpret the index as indexers, default False
|
||||
|
||||
Returns
|
||||
-------
|
||||
value : scalar value
|
||||
"""
|
||||
warnings.warn("get_value is deprecated and will be removed "
|
||||
"in a future release. Please use "
|
||||
".at[] or .iat[] accessors instead", FutureWarning,
|
||||
stacklevel=2)
|
||||
|
||||
return self._get_value(label, takeable=takeable)
|
||||
|
||||
def _get_value(self, label, takeable=False):
|
||||
loc = label if takeable is True else self.index.get_loc(label)
|
||||
return self._get_val_at(loc)
|
||||
_get_value.__doc__ = get_value.__doc__
|
||||
|
||||
def set_value(self, label, value, takeable=False):
|
||||
"""
|
||||
Quickly set single value at passed label. If label is not contained, a
|
||||
new object is created with the label placed at the end of the result
|
||||
index
|
||||
|
||||
.. deprecated:: 0.21.0
|
||||
|
||||
Please use .at[] or .iat[] accessors.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : object
|
||||
Partial indexing with MultiIndex not allowed
|
||||
value : object
|
||||
Scalar value
|
||||
takeable : interpret the index as indexers, default False
|
||||
|
||||
Notes
|
||||
-----
|
||||
This method *always* returns a new object. It is not particularly
|
||||
efficient but is provided for API compatibility with Series
|
||||
|
||||
Returns
|
||||
-------
|
||||
series : SparseSeries
|
||||
"""
|
||||
warnings.warn("set_value is deprecated and will be removed "
|
||||
"in a future release. Please use "
|
||||
".at[] or .iat[] accessors instead", FutureWarning,
|
||||
stacklevel=2)
|
||||
return self._set_value(label, value, takeable=takeable)
|
||||
|
||||
def _set_value(self, label, value, takeable=False):
|
||||
values = self.to_dense()
|
||||
|
||||
# if the label doesn't exist, we will create a new object here
|
||||
# and possibly change the index
|
||||
new_values = values._set_value(label, value, takeable=takeable)
|
||||
if new_values is not None:
|
||||
values = new_values
|
||||
new_index = values.index
|
||||
values = SparseArray(values, fill_value=self.fill_value,
|
||||
kind=self.kind)
|
||||
self._data = SingleBlockManager(values, new_index)
|
||||
self._index = new_index
|
||||
_set_value.__doc__ = set_value.__doc__
|
||||
|
||||
def _set_values(self, key, value):
|
||||
|
||||
# this might be inefficient as we have to recreate the sparse array
|
||||
# rather than setting individual elements, but have to convert
|
||||
# the passed slice/boolean that's in dense space into a sparse indexer
|
||||
# not sure how to do that!
|
||||
if isinstance(key, Series):
|
||||
key = key.values
|
||||
|
||||
values = self.values.to_dense()
|
||||
values[key] = libindex.convert_scalar(values, value)
|
||||
values = SparseArray(values, fill_value=self.fill_value,
|
||||
kind=self.kind)
|
||||
self._data = SingleBlockManager(values, self.index)
|
||||
|
||||
def to_dense(self, sparse_only=False):
|
||||
"""
|
||||
Convert SparseSeries to a Series.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
sparse_only : bool, default False
|
||||
.. deprecated:: 0.20.0
|
||||
This argument will be removed in a future version.
|
||||
|
||||
If True, return just the non-sparse values, or the dense version
|
||||
of `self.values` if False.
|
||||
|
||||
Returns
|
||||
-------
|
||||
s : Series
|
||||
"""
|
||||
if sparse_only:
|
||||
warnings.warn(("The 'sparse_only' parameter has been deprecated "
|
||||
"and will be removed in a future version."),
|
||||
FutureWarning, stacklevel=2)
|
||||
int_index = self.sp_index.to_int_index()
|
||||
index = self.index.take(int_index.indices)
|
||||
return Series(self.sp_values, index=index, name=self.name)
|
||||
else:
|
||||
return Series(self.values.to_dense(), index=self.index,
|
||||
name=self.name)
|
||||
|
||||
@property
|
||||
def density(self):
|
||||
r = float(self.sp_index.npoints) / float(self.sp_index.length)
|
||||
return r
|
||||
|
||||
def copy(self, deep=True):
|
||||
"""
|
||||
Make a copy of the SparseSeries. Only the actual sparse values need to
|
||||
be copied
|
||||
"""
|
||||
new_data = self._data
|
||||
if deep:
|
||||
new_data = self._data.copy()
|
||||
|
||||
return self._constructor(new_data, sparse_index=self.sp_index,
|
||||
fill_value=self.fill_value).__finalize__(self)
|
||||
|
||||
@Appender(generic._shared_docs['reindex'] % _shared_doc_kwargs)
|
||||
def reindex(self, index=None, method=None, copy=True, limit=None,
|
||||
**kwargs):
|
||||
|
||||
return super(SparseSeries, self).reindex(index=index, method=method,
|
||||
copy=copy, limit=limit,
|
||||
**kwargs)
|
||||
|
||||
def sparse_reindex(self, new_index):
|
||||
"""
|
||||
Conform sparse values to new SparseIndex
|
||||
|
||||
Parameters
|
||||
----------
|
||||
new_index : {BlockIndex, IntIndex}
|
||||
|
||||
Returns
|
||||
-------
|
||||
reindexed : SparseSeries
|
||||
"""
|
||||
if not isinstance(new_index, splib.SparseIndex):
|
||||
raise TypeError('new index must be a SparseIndex')
|
||||
|
||||
block = self.block.sparse_reindex(new_index)
|
||||
new_data = SingleBlockManager(block, self.index)
|
||||
return self._constructor(new_data, index=self.index,
|
||||
sparse_index=new_index,
|
||||
fill_value=self.fill_value).__finalize__(self)
|
||||
|
||||
@Appender(generic._shared_docs['take'])
|
||||
def take(self, indices, axis=0, convert=None, *args, **kwargs):
|
||||
if convert is not None:
|
||||
msg = ("The 'convert' parameter is deprecated "
|
||||
"and will be removed in a future version.")
|
||||
warnings.warn(msg, FutureWarning, stacklevel=2)
|
||||
else:
|
||||
convert = True
|
||||
|
||||
nv.validate_take_with_convert(convert, args, kwargs)
|
||||
new_values = SparseArray.take(self.values, indices)
|
||||
new_index = self.index.take(indices)
|
||||
return self._constructor(new_values,
|
||||
index=new_index).__finalize__(self)
|
||||
|
||||
def cumsum(self, axis=0, *args, **kwargs):
|
||||
"""
|
||||
Cumulative sum of non-NA/null values.
|
||||
|
||||
When performing the cumulative summation, any non-NA/null values will
|
||||
be skipped. The resulting SparseSeries will preserve the locations of
|
||||
NaN values, but the fill value will be `np.nan` regardless.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
axis : {0}
|
||||
|
||||
Returns
|
||||
-------
|
||||
cumsum : SparseSeries
|
||||
"""
|
||||
nv.validate_cumsum(args, kwargs)
|
||||
if axis is not None:
|
||||
axis = self._get_axis_number(axis)
|
||||
|
||||
new_array = self.values.cumsum()
|
||||
|
||||
return self._constructor(
|
||||
new_array, index=self.index,
|
||||
sparse_index=new_array.sp_index).__finalize__(self)
|
||||
|
||||
@Appender(generic._shared_docs['isna'] % _shared_doc_kwargs)
|
||||
def isna(self):
|
||||
arr = SparseArray(isna(self.values.sp_values),
|
||||
sparse_index=self.values.sp_index,
|
||||
fill_value=isna(self.fill_value))
|
||||
return self._constructor(arr, index=self.index).__finalize__(self)
|
||||
isnull = isna
|
||||
|
||||
@Appender(generic._shared_docs['notna'] % _shared_doc_kwargs)
|
||||
def notna(self):
|
||||
arr = SparseArray(notna(self.values.sp_values),
|
||||
sparse_index=self.values.sp_index,
|
||||
fill_value=notna(self.fill_value))
|
||||
return self._constructor(arr, index=self.index).__finalize__(self)
|
||||
notnull = notna
|
||||
|
||||
def dropna(self, axis=0, inplace=False, **kwargs):
|
||||
"""
|
||||
Analogous to Series.dropna. If fill_value=NaN, returns a dense Series
|
||||
"""
|
||||
# TODO: make more efficient
|
||||
axis = self._get_axis_number(axis or 0)
|
||||
dense_valid = self.to_dense().dropna()
|
||||
if inplace:
|
||||
raise NotImplementedError("Cannot perform inplace dropna"
|
||||
" operations on a SparseSeries")
|
||||
if isna(self.fill_value):
|
||||
return dense_valid
|
||||
else:
|
||||
dense_valid = dense_valid[dense_valid != self.fill_value]
|
||||
return dense_valid.to_sparse(fill_value=self.fill_value)
|
||||
|
||||
@Appender(generic._shared_docs['shift'] % _shared_doc_kwargs)
|
||||
def shift(self, periods, freq=None, axis=0):
|
||||
if periods == 0:
|
||||
return self.copy()
|
||||
|
||||
# no special handling of fill values yet
|
||||
if not isna(self.fill_value):
|
||||
shifted = self.to_dense().shift(periods, freq=freq,
|
||||
axis=axis)
|
||||
return shifted.to_sparse(fill_value=self.fill_value,
|
||||
kind=self.kind)
|
||||
|
||||
if freq is not None:
|
||||
return self._constructor(
|
||||
self.sp_values, sparse_index=self.sp_index,
|
||||
index=self.index.shift(periods, freq),
|
||||
fill_value=self.fill_value).__finalize__(self)
|
||||
|
||||
int_index = self.sp_index.to_int_index()
|
||||
new_indices = int_index.indices + periods
|
||||
start, end = new_indices.searchsorted([0, int_index.length])
|
||||
|
||||
new_indices = new_indices[start:end]
|
||||
new_sp_index = _make_index(len(self), new_indices, self.sp_index)
|
||||
|
||||
arr = self.values._simple_new(self.sp_values[start:end].copy(),
|
||||
new_sp_index, fill_value=np.nan)
|
||||
return self._constructor(arr, index=self.index).__finalize__(self)
|
||||
|
||||
def combine_first(self, other):
|
||||
"""
|
||||
Combine Series values, choosing the calling Series's values
|
||||
first. Result index will be the union of the two indexes
|
||||
|
||||
Parameters
|
||||
----------
|
||||
other : Series
|
||||
|
||||
Returns
|
||||
-------
|
||||
y : Series
|
||||
"""
|
||||
if isinstance(other, SparseSeries):
|
||||
other = other.to_dense()
|
||||
|
||||
dense_combined = self.to_dense().combine_first(other)
|
||||
return dense_combined.to_sparse(fill_value=self.fill_value)
|
||||
|
||||
def to_coo(self, row_levels=(0, ), column_levels=(1, ), sort_labels=False):
|
||||
"""
|
||||
Create a scipy.sparse.coo_matrix from a SparseSeries with MultiIndex.
|
||||
|
||||
Use row_levels and column_levels to determine the row and column
|
||||
coordinates respectively. row_levels and column_levels are the names
|
||||
(labels) or numbers of the levels. {row_levels, column_levels} must be
|
||||
a partition of the MultiIndex level names (or numbers).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
row_levels : tuple/list
|
||||
column_levels : tuple/list
|
||||
sort_labels : bool, default False
|
||||
Sort the row and column labels before forming the sparse matrix.
|
||||
|
||||
Returns
|
||||
-------
|
||||
y : scipy.sparse.coo_matrix
|
||||
rows : list (row labels)
|
||||
columns : list (column labels)
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from numpy import nan
|
||||
>>> s = Series([3.0, nan, 1.0, 3.0, nan, nan])
|
||||
>>> s.index = MultiIndex.from_tuples([(1, 2, 'a', 0),
|
||||
(1, 2, 'a', 1),
|
||||
(1, 1, 'b', 0),
|
||||
(1, 1, 'b', 1),
|
||||
(2, 1, 'b', 0),
|
||||
(2, 1, 'b', 1)],
|
||||
names=['A', 'B', 'C', 'D'])
|
||||
>>> ss = s.to_sparse()
|
||||
>>> A, rows, columns = ss.to_coo(row_levels=['A', 'B'],
|
||||
column_levels=['C', 'D'],
|
||||
sort_labels=True)
|
||||
>>> A
|
||||
<3x4 sparse matrix of type '<class 'numpy.float64'>'
|
||||
with 3 stored elements in COOrdinate format>
|
||||
>>> A.todense()
|
||||
matrix([[ 0., 0., 1., 3.],
|
||||
[ 3., 0., 0., 0.],
|
||||
[ 0., 0., 0., 0.]])
|
||||
>>> rows
|
||||
[(1, 1), (1, 2), (2, 1)]
|
||||
>>> columns
|
||||
[('a', 0), ('a', 1), ('b', 0), ('b', 1)]
|
||||
"""
|
||||
A, rows, columns = _sparse_series_to_coo(self, row_levels,
|
||||
column_levels,
|
||||
sort_labels=sort_labels)
|
||||
return A, rows, columns
|
||||
|
||||
@classmethod
|
||||
def from_coo(cls, A, dense_index=False):
|
||||
"""
|
||||
Create a SparseSeries from a scipy.sparse.coo_matrix.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : scipy.sparse.coo_matrix
|
||||
dense_index : bool, default False
|
||||
If False (default), the SparseSeries index consists of only the
|
||||
coords of the non-null entries of the original coo_matrix.
|
||||
If True, the SparseSeries index consists of the full sorted
|
||||
(row, col) coordinates of the coo_matrix.
|
||||
|
||||
Returns
|
||||
-------
|
||||
s : SparseSeries
|
||||
|
||||
Examples
|
||||
---------
|
||||
>>> from scipy import sparse
|
||||
>>> A = sparse.coo_matrix(([3.0, 1.0, 2.0], ([1, 0, 0], [0, 2, 3])),
|
||||
shape=(3, 4))
|
||||
>>> A
|
||||
<3x4 sparse matrix of type '<class 'numpy.float64'>'
|
||||
with 3 stored elements in COOrdinate format>
|
||||
>>> A.todense()
|
||||
matrix([[ 0., 0., 1., 2.],
|
||||
[ 3., 0., 0., 0.],
|
||||
[ 0., 0., 0., 0.]])
|
||||
>>> ss = SparseSeries.from_coo(A)
|
||||
>>> ss
|
||||
0 2 1
|
||||
3 2
|
||||
1 0 3
|
||||
dtype: float64
|
||||
BlockIndex
|
||||
Block locations: array([0], dtype=int32)
|
||||
Block lengths: array([3], dtype=int32)
|
||||
"""
|
||||
return _coo_to_sparse_series(A, dense_index=dense_index)
|
||||
|
||||
|
||||
# overwrite series methods with unaccelerated Sparse-specific versions
|
||||
ops.add_flex_arithmetic_methods(SparseSeries)
|
||||
ops.add_special_arithmetic_methods(SparseSeries)
|
||||
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,774 @@
|
||||
from datetime import datetime, timedelta, time
|
||||
import numpy as np
|
||||
from collections import MutableMapping
|
||||
|
||||
from pandas._libs import tslib
|
||||
from pandas._libs.tslibs.strptime import array_strptime
|
||||
from pandas._libs.tslibs import parsing, conversion
|
||||
from pandas._libs.tslibs.parsing import ( # noqa
|
||||
parse_time_string,
|
||||
DateParseError,
|
||||
_format_is_iso,
|
||||
_guess_datetime_format)
|
||||
|
||||
from pandas.core.dtypes.common import (
|
||||
_ensure_object,
|
||||
is_datetime64_ns_dtype,
|
||||
is_datetime64_dtype,
|
||||
is_datetime64tz_dtype,
|
||||
is_integer_dtype,
|
||||
is_integer,
|
||||
is_float,
|
||||
is_list_like,
|
||||
is_scalar,
|
||||
is_numeric_dtype)
|
||||
from pandas.core.dtypes.generic import (
|
||||
ABCIndexClass, ABCSeries,
|
||||
ABCDataFrame)
|
||||
from pandas.core.dtypes.missing import notna
|
||||
from pandas.core import algorithms
|
||||
|
||||
|
||||
def _guess_datetime_format_for_array(arr, **kwargs):
|
||||
# Try to guess the format based on the first non-NaN element
|
||||
non_nan_elements = notna(arr).nonzero()[0]
|
||||
if len(non_nan_elements):
|
||||
return _guess_datetime_format(arr[non_nan_elements[0]], **kwargs)
|
||||
|
||||
|
||||
def _maybe_cache(arg, format, cache, tz, convert_listlike):
|
||||
"""
|
||||
Create a cache of unique dates from an array of dates
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arg : integer, float, string, datetime, list, tuple, 1-d array, Series
|
||||
format : string
|
||||
Strftime format to parse time
|
||||
cache : boolean
|
||||
True attempts to create a cache of converted values
|
||||
tz : string
|
||||
Timezone of the dates
|
||||
convert_listlike : function
|
||||
Conversion function to apply on dates
|
||||
|
||||
Returns
|
||||
-------
|
||||
cache_array : Series
|
||||
Cache of converted, unique dates. Can be empty
|
||||
"""
|
||||
from pandas import Series
|
||||
cache_array = Series()
|
||||
if cache:
|
||||
# Perform a quicker unique check
|
||||
from pandas import Index
|
||||
if not Index(arg).is_unique:
|
||||
unique_dates = algorithms.unique(arg)
|
||||
cache_dates = convert_listlike(unique_dates, True, format, tz=tz)
|
||||
cache_array = Series(cache_dates, index=unique_dates)
|
||||
return cache_array
|
||||
|
||||
|
||||
def _convert_and_box_cache(arg, cache_array, box, errors, name=None):
|
||||
"""
|
||||
Convert array of dates with a cache and box the result
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arg : integer, float, string, datetime, list, tuple, 1-d array, Series
|
||||
cache_array : Series
|
||||
Cache of converted, unique dates
|
||||
box : boolean
|
||||
True boxes result as an Index-like, False returns an ndarray
|
||||
errors : string
|
||||
'ignore' plus box=True will convert result to Index
|
||||
name : string, default None
|
||||
Name for a DatetimeIndex
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : datetime of converted dates
|
||||
Returns:
|
||||
|
||||
- Index-like if box=True
|
||||
- ndarray if box=False
|
||||
"""
|
||||
from pandas import Series, DatetimeIndex, Index
|
||||
result = Series(arg).map(cache_array)
|
||||
if box:
|
||||
if errors == 'ignore':
|
||||
return Index(result)
|
||||
else:
|
||||
return DatetimeIndex(result, name=name)
|
||||
return result.values
|
||||
|
||||
|
||||
def to_datetime(arg, errors='raise', dayfirst=False, yearfirst=False,
|
||||
utc=None, box=True, format=None, exact=True,
|
||||
unit=None, infer_datetime_format=False, origin='unix',
|
||||
cache=False):
|
||||
"""
|
||||
Convert argument to datetime.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arg : integer, float, string, datetime, list, tuple, 1-d array, Series
|
||||
|
||||
.. versionadded:: 0.18.1
|
||||
|
||||
or DataFrame/dict-like
|
||||
|
||||
errors : {'ignore', 'raise', 'coerce'}, default 'raise'
|
||||
|
||||
- If 'raise', then invalid parsing will raise an exception
|
||||
- If 'coerce', then invalid parsing will be set as NaT
|
||||
- If 'ignore', then invalid parsing will return the input
|
||||
dayfirst : boolean, default False
|
||||
Specify a date parse order if `arg` is str or its list-likes.
|
||||
If True, parses dates with the day first, eg 10/11/12 is parsed as
|
||||
2012-11-10.
|
||||
Warning: dayfirst=True is not strict, but will prefer to parse
|
||||
with day first (this is a known bug, based on dateutil behavior).
|
||||
yearfirst : boolean, default False
|
||||
Specify a date parse order if `arg` is str or its list-likes.
|
||||
|
||||
- If True parses dates with the year first, eg 10/11/12 is parsed as
|
||||
2010-11-12.
|
||||
- If both dayfirst and yearfirst are True, yearfirst is preceded (same
|
||||
as dateutil).
|
||||
|
||||
Warning: yearfirst=True is not strict, but will prefer to parse
|
||||
with year first (this is a known bug, based on dateutil beahavior).
|
||||
|
||||
.. versionadded:: 0.16.1
|
||||
|
||||
utc : boolean, default None
|
||||
Return UTC DatetimeIndex if True (converting any tz-aware
|
||||
datetime.datetime objects as well).
|
||||
box : boolean, default True
|
||||
|
||||
- If True returns a DatetimeIndex
|
||||
- If False returns ndarray of values.
|
||||
format : string, default None
|
||||
strftime to parse time, eg "%d/%m/%Y", note that "%f" will parse
|
||||
all the way up to nanoseconds.
|
||||
exact : boolean, True by default
|
||||
|
||||
- If True, require an exact format match.
|
||||
- If False, allow the format to match anywhere in the target string.
|
||||
|
||||
unit : string, default 'ns'
|
||||
unit of the arg (D,s,ms,us,ns) denote the unit, which is an
|
||||
integer or float number. This will be based off the origin.
|
||||
Example, with unit='ms' and origin='unix' (the default), this
|
||||
would calculate the number of milliseconds to the unix epoch start.
|
||||
infer_datetime_format : boolean, default False
|
||||
If True and no `format` is given, attempt to infer the format of the
|
||||
datetime strings, and if it can be inferred, switch to a faster
|
||||
method of parsing them. In some cases this can increase the parsing
|
||||
speed by ~5-10x.
|
||||
origin : scalar, default is 'unix'
|
||||
Define the reference date. The numeric values would be parsed as number
|
||||
of units (defined by `unit`) since this reference date.
|
||||
|
||||
- If 'unix' (or POSIX) time; origin is set to 1970-01-01.
|
||||
- If 'julian', unit must be 'D', and origin is set to beginning of
|
||||
Julian Calendar. Julian day number 0 is assigned to the day starting
|
||||
at noon on January 1, 4713 BC.
|
||||
- If Timestamp convertible, origin is set to Timestamp identified by
|
||||
origin.
|
||||
|
||||
.. versionadded:: 0.20.0
|
||||
cache : boolean, default False
|
||||
If True, use a cache of unique, converted dates to apply the datetime
|
||||
conversion. May produce sigificant speed-up when parsing duplicate date
|
||||
strings, especially ones with timezone offsets.
|
||||
|
||||
.. versionadded:: 0.23.0
|
||||
|
||||
Returns
|
||||
-------
|
||||
ret : datetime if parsing succeeded.
|
||||
Return type depends on input:
|
||||
|
||||
- list-like: DatetimeIndex
|
||||
- Series: Series of datetime64 dtype
|
||||
- scalar: Timestamp
|
||||
|
||||
In case when it is not possible to return designated types (e.g. when
|
||||
any element of input is before Timestamp.min or after Timestamp.max)
|
||||
return will have datetime.datetime type (or corresponding
|
||||
array/Series).
|
||||
|
||||
Examples
|
||||
--------
|
||||
Assembling a datetime from multiple columns of a DataFrame. The keys can be
|
||||
common abbreviations like ['year', 'month', 'day', 'minute', 'second',
|
||||
'ms', 'us', 'ns']) or plurals of the same
|
||||
|
||||
>>> df = pd.DataFrame({'year': [2015, 2016],
|
||||
'month': [2, 3],
|
||||
'day': [4, 5]})
|
||||
>>> pd.to_datetime(df)
|
||||
0 2015-02-04
|
||||
1 2016-03-05
|
||||
dtype: datetime64[ns]
|
||||
|
||||
If a date does not meet the `timestamp limitations
|
||||
<http://pandas.pydata.org/pandas-docs/stable/timeseries.html
|
||||
#timeseries-timestamp-limits>`_, passing errors='ignore'
|
||||
will return the original input instead of raising any exception.
|
||||
|
||||
Passing errors='coerce' will force an out-of-bounds date to NaT,
|
||||
in addition to forcing non-dates (or non-parseable dates) to NaT.
|
||||
|
||||
>>> pd.to_datetime('13000101', format='%Y%m%d', errors='ignore')
|
||||
datetime.datetime(1300, 1, 1, 0, 0)
|
||||
>>> pd.to_datetime('13000101', format='%Y%m%d', errors='coerce')
|
||||
NaT
|
||||
|
||||
Passing infer_datetime_format=True can often-times speedup a parsing
|
||||
if its not an ISO8601 format exactly, but in a regular format.
|
||||
|
||||
>>> s = pd.Series(['3/11/2000', '3/12/2000', '3/13/2000']*1000)
|
||||
|
||||
>>> s.head()
|
||||
0 3/11/2000
|
||||
1 3/12/2000
|
||||
2 3/13/2000
|
||||
3 3/11/2000
|
||||
4 3/12/2000
|
||||
dtype: object
|
||||
|
||||
>>> %timeit pd.to_datetime(s,infer_datetime_format=True)
|
||||
100 loops, best of 3: 10.4 ms per loop
|
||||
|
||||
>>> %timeit pd.to_datetime(s,infer_datetime_format=False)
|
||||
1 loop, best of 3: 471 ms per loop
|
||||
|
||||
Using a unix epoch time
|
||||
|
||||
>>> pd.to_datetime(1490195805, unit='s')
|
||||
Timestamp('2017-03-22 15:16:45')
|
||||
>>> pd.to_datetime(1490195805433502912, unit='ns')
|
||||
Timestamp('2017-03-22 15:16:45.433502912')
|
||||
|
||||
.. warning:: For float arg, precision rounding might happen. To prevent
|
||||
unexpected behavior use a fixed-width exact type.
|
||||
|
||||
Using a non-unix epoch origin
|
||||
|
||||
>>> pd.to_datetime([1, 2, 3], unit='D',
|
||||
origin=pd.Timestamp('1960-01-01'))
|
||||
0 1960-01-02
|
||||
1 1960-01-03
|
||||
2 1960-01-04
|
||||
|
||||
See also
|
||||
--------
|
||||
pandas.DataFrame.astype : Cast argument to a specified dtype.
|
||||
pandas.to_timedelta : Convert argument to timedelta.
|
||||
"""
|
||||
from pandas.core.indexes.datetimes import DatetimeIndex
|
||||
|
||||
tz = 'utc' if utc else None
|
||||
|
||||
def _convert_listlike(arg, box, format, name=None, tz=tz):
|
||||
|
||||
if isinstance(arg, (list, tuple)):
|
||||
arg = np.array(arg, dtype='O')
|
||||
|
||||
# these are shortcutable
|
||||
if is_datetime64tz_dtype(arg):
|
||||
if not isinstance(arg, DatetimeIndex):
|
||||
return DatetimeIndex(arg, tz=tz, name=name)
|
||||
if utc:
|
||||
arg = arg.tz_convert(None).tz_localize('UTC')
|
||||
return arg
|
||||
|
||||
elif is_datetime64_ns_dtype(arg):
|
||||
if box and not isinstance(arg, DatetimeIndex):
|
||||
try:
|
||||
return DatetimeIndex(arg, tz=tz, name=name)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return arg
|
||||
|
||||
elif unit is not None:
|
||||
if format is not None:
|
||||
raise ValueError("cannot specify both format and unit")
|
||||
arg = getattr(arg, 'values', arg)
|
||||
result = tslib.array_with_unit_to_datetime(arg, unit,
|
||||
errors=errors)
|
||||
if box:
|
||||
if errors == 'ignore':
|
||||
from pandas import Index
|
||||
return Index(result)
|
||||
|
||||
return DatetimeIndex(result, tz=tz, name=name)
|
||||
return result
|
||||
elif getattr(arg, 'ndim', 1) > 1:
|
||||
raise TypeError('arg must be a string, datetime, list, tuple, '
|
||||
'1-d array, or Series')
|
||||
|
||||
arg = _ensure_object(arg)
|
||||
require_iso8601 = False
|
||||
|
||||
if infer_datetime_format and format is None:
|
||||
format = _guess_datetime_format_for_array(arg, dayfirst=dayfirst)
|
||||
|
||||
if format is not None:
|
||||
# There is a special fast-path for iso8601 formatted
|
||||
# datetime strings, so in those cases don't use the inferred
|
||||
# format because this path makes process slower in this
|
||||
# special case
|
||||
format_is_iso8601 = _format_is_iso(format)
|
||||
if format_is_iso8601:
|
||||
require_iso8601 = not infer_datetime_format
|
||||
format = None
|
||||
|
||||
try:
|
||||
result = None
|
||||
|
||||
if format is not None:
|
||||
# shortcut formatting here
|
||||
if format == '%Y%m%d':
|
||||
try:
|
||||
result = _attempt_YYYYMMDD(arg, errors=errors)
|
||||
except:
|
||||
raise ValueError("cannot convert the input to "
|
||||
"'%Y%m%d' date format")
|
||||
|
||||
# fallback
|
||||
if result is None:
|
||||
try:
|
||||
result = array_strptime(arg, format, exact=exact,
|
||||
errors=errors)
|
||||
except tslib.OutOfBoundsDatetime:
|
||||
if errors == 'raise':
|
||||
raise
|
||||
result = arg
|
||||
except ValueError:
|
||||
# if format was inferred, try falling back
|
||||
# to array_to_datetime - terminate here
|
||||
# for specified formats
|
||||
if not infer_datetime_format:
|
||||
if errors == 'raise':
|
||||
raise
|
||||
result = arg
|
||||
|
||||
if result is None and (format is None or infer_datetime_format):
|
||||
result = tslib.array_to_datetime(
|
||||
arg,
|
||||
errors=errors,
|
||||
utc=utc,
|
||||
dayfirst=dayfirst,
|
||||
yearfirst=yearfirst,
|
||||
require_iso8601=require_iso8601
|
||||
)
|
||||
|
||||
if is_datetime64_dtype(result) and box:
|
||||
result = DatetimeIndex(result, tz=tz, name=name)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
try:
|
||||
values, tz = conversion.datetime_to_datetime64(arg)
|
||||
return DatetimeIndex._simple_new(values, name=name, tz=tz)
|
||||
except (ValueError, TypeError):
|
||||
raise e
|
||||
|
||||
if arg is None:
|
||||
return None
|
||||
|
||||
# handle origin
|
||||
if origin == 'julian':
|
||||
|
||||
original = arg
|
||||
j0 = tslib.Timestamp(0).to_julian_date()
|
||||
if unit != 'D':
|
||||
raise ValueError("unit must be 'D' for origin='julian'")
|
||||
try:
|
||||
arg = arg - j0
|
||||
except:
|
||||
raise ValueError("incompatible 'arg' type for given "
|
||||
"'origin'='julian'")
|
||||
|
||||
# premptively check this for a nice range
|
||||
j_max = tslib.Timestamp.max.to_julian_date() - j0
|
||||
j_min = tslib.Timestamp.min.to_julian_date() - j0
|
||||
if np.any(arg > j_max) or np.any(arg < j_min):
|
||||
raise tslib.OutOfBoundsDatetime(
|
||||
"{original} is Out of Bounds for "
|
||||
"origin='julian'".format(original=original))
|
||||
|
||||
elif origin not in ['unix', 'julian']:
|
||||
|
||||
# arg must be a numeric
|
||||
original = arg
|
||||
if not ((is_scalar(arg) and (is_integer(arg) or is_float(arg))) or
|
||||
is_numeric_dtype(np.asarray(arg))):
|
||||
raise ValueError(
|
||||
"'{arg}' is not compatible with origin='{origin}'; "
|
||||
"it must be numeric with a unit specified ".format(
|
||||
arg=arg,
|
||||
origin=origin))
|
||||
|
||||
# we are going to offset back to unix / epoch time
|
||||
try:
|
||||
offset = tslib.Timestamp(origin)
|
||||
except tslib.OutOfBoundsDatetime:
|
||||
raise tslib.OutOfBoundsDatetime(
|
||||
"origin {origin} is Out of Bounds".format(origin=origin))
|
||||
except ValueError:
|
||||
raise ValueError("origin {origin} cannot be converted "
|
||||
"to a Timestamp".format(origin=origin))
|
||||
|
||||
if offset.tz is not None:
|
||||
raise ValueError(
|
||||
"origin offset {} must be tz-naive".format(offset))
|
||||
offset -= tslib.Timestamp(0)
|
||||
|
||||
# convert the offset to the unit of the arg
|
||||
# this should be lossless in terms of precision
|
||||
offset = offset // tslib.Timedelta(1, unit=unit)
|
||||
|
||||
# scalars & ndarray-like can handle the addition
|
||||
if is_list_like(arg) and not isinstance(
|
||||
arg, (ABCSeries, ABCIndexClass, np.ndarray)):
|
||||
arg = np.asarray(arg)
|
||||
arg = arg + offset
|
||||
|
||||
if isinstance(arg, tslib.Timestamp):
|
||||
result = arg
|
||||
elif isinstance(arg, ABCSeries):
|
||||
cache_array = _maybe_cache(arg, format, cache, tz, _convert_listlike)
|
||||
if not cache_array.empty:
|
||||
result = arg.map(cache_array)
|
||||
else:
|
||||
from pandas import Series
|
||||
values = _convert_listlike(arg._values, True, format)
|
||||
result = Series(values, index=arg.index, name=arg.name)
|
||||
elif isinstance(arg, (ABCDataFrame, MutableMapping)):
|
||||
result = _assemble_from_unit_mappings(arg, errors=errors)
|
||||
elif isinstance(arg, ABCIndexClass):
|
||||
cache_array = _maybe_cache(arg, format, cache, tz, _convert_listlike)
|
||||
if not cache_array.empty:
|
||||
result = _convert_and_box_cache(arg, cache_array, box, errors,
|
||||
name=arg.name)
|
||||
else:
|
||||
result = _convert_listlike(arg, box, format, name=arg.name)
|
||||
elif is_list_like(arg):
|
||||
cache_array = _maybe_cache(arg, format, cache, tz, _convert_listlike)
|
||||
if not cache_array.empty:
|
||||
result = _convert_and_box_cache(arg, cache_array, box, errors)
|
||||
else:
|
||||
result = _convert_listlike(arg, box, format)
|
||||
else:
|
||||
result = _convert_listlike(np.array([arg]), box, format)[0]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# mappings for assembling units
|
||||
_unit_map = {'year': 'year',
|
||||
'years': 'year',
|
||||
'month': 'month',
|
||||
'months': 'month',
|
||||
'day': 'day',
|
||||
'days': 'day',
|
||||
'hour': 'h',
|
||||
'hours': 'h',
|
||||
'minute': 'm',
|
||||
'minutes': 'm',
|
||||
'second': 's',
|
||||
'seconds': 's',
|
||||
'ms': 'ms',
|
||||
'millisecond': 'ms',
|
||||
'milliseconds': 'ms',
|
||||
'us': 'us',
|
||||
'microsecond': 'us',
|
||||
'microseconds': 'us',
|
||||
'ns': 'ns',
|
||||
'nanosecond': 'ns',
|
||||
'nanoseconds': 'ns'
|
||||
}
|
||||
|
||||
|
||||
def _assemble_from_unit_mappings(arg, errors):
|
||||
"""
|
||||
assemble the unit specified fields from the arg (DataFrame)
|
||||
Return a Series for actual parsing
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arg : DataFrame
|
||||
errors : {'ignore', 'raise', 'coerce'}, default 'raise'
|
||||
|
||||
- If 'raise', then invalid parsing will raise an exception
|
||||
- If 'coerce', then invalid parsing will be set as NaT
|
||||
- If 'ignore', then invalid parsing will return the input
|
||||
|
||||
Returns
|
||||
-------
|
||||
Series
|
||||
"""
|
||||
from pandas import to_timedelta, to_numeric, DataFrame
|
||||
arg = DataFrame(arg)
|
||||
if not arg.columns.is_unique:
|
||||
raise ValueError("cannot assemble with duplicate keys")
|
||||
|
||||
# replace passed unit with _unit_map
|
||||
def f(value):
|
||||
if value in _unit_map:
|
||||
return _unit_map[value]
|
||||
|
||||
# m is case significant
|
||||
if value.lower() in _unit_map:
|
||||
return _unit_map[value.lower()]
|
||||
|
||||
return value
|
||||
|
||||
unit = {k: f(k) for k in arg.keys()}
|
||||
unit_rev = {v: k for k, v in unit.items()}
|
||||
|
||||
# we require at least Ymd
|
||||
required = ['year', 'month', 'day']
|
||||
req = sorted(list(set(required) - set(unit_rev.keys())))
|
||||
if len(req):
|
||||
raise ValueError("to assemble mappings requires at least that "
|
||||
"[year, month, day] be specified: [{required}] "
|
||||
"is missing".format(required=','.join(req)))
|
||||
|
||||
# keys we don't recognize
|
||||
excess = sorted(list(set(unit_rev.keys()) - set(_unit_map.values())))
|
||||
if len(excess):
|
||||
raise ValueError("extra keys have been passed "
|
||||
"to the datetime assemblage: "
|
||||
"[{excess}]".format(excess=','.join(excess)))
|
||||
|
||||
def coerce(values):
|
||||
# we allow coercion to if errors allows
|
||||
values = to_numeric(values, errors=errors)
|
||||
|
||||
# prevent overflow in case of int8 or int16
|
||||
if is_integer_dtype(values):
|
||||
values = values.astype('int64', copy=False)
|
||||
return values
|
||||
|
||||
values = (coerce(arg[unit_rev['year']]) * 10000 +
|
||||
coerce(arg[unit_rev['month']]) * 100 +
|
||||
coerce(arg[unit_rev['day']]))
|
||||
try:
|
||||
values = to_datetime(values, format='%Y%m%d', errors=errors)
|
||||
except (TypeError, ValueError) as e:
|
||||
raise ValueError("cannot assemble the "
|
||||
"datetimes: {error}".format(error=e))
|
||||
|
||||
for u in ['h', 'm', 's', 'ms', 'us', 'ns']:
|
||||
value = unit_rev.get(u)
|
||||
if value is not None and value in arg:
|
||||
try:
|
||||
values += to_timedelta(coerce(arg[value]),
|
||||
unit=u,
|
||||
errors=errors)
|
||||
except (TypeError, ValueError) as e:
|
||||
raise ValueError("cannot assemble the datetimes [{value}]: "
|
||||
"{error}".format(value=value, error=e))
|
||||
|
||||
return values
|
||||
|
||||
|
||||
def _attempt_YYYYMMDD(arg, errors):
|
||||
""" try to parse the YYYYMMDD/%Y%m%d format, try to deal with NaT-like,
|
||||
arg is a passed in as an object dtype, but could really be ints/strings
|
||||
with nan-like/or floats (e.g. with nan)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arg : passed value
|
||||
errors : 'raise','ignore','coerce'
|
||||
"""
|
||||
|
||||
def calc(carg):
|
||||
# calculate the actual result
|
||||
carg = carg.astype(object)
|
||||
parsed = parsing.try_parse_year_month_day(carg / 10000,
|
||||
carg / 100 % 100,
|
||||
carg % 100)
|
||||
return tslib.array_to_datetime(parsed, errors=errors)
|
||||
|
||||
def calc_with_mask(carg, mask):
|
||||
result = np.empty(carg.shape, dtype='M8[ns]')
|
||||
iresult = result.view('i8')
|
||||
iresult[~mask] = tslib.iNaT
|
||||
result[mask] = calc(carg[mask].astype(np.float64).astype(np.int64)). \
|
||||
astype('M8[ns]')
|
||||
return result
|
||||
|
||||
# try intlike / strings that are ints
|
||||
try:
|
||||
return calc(arg.astype(np.int64))
|
||||
except:
|
||||
pass
|
||||
|
||||
# a float with actual np.nan
|
||||
try:
|
||||
carg = arg.astype(np.float64)
|
||||
return calc_with_mask(carg, notna(carg))
|
||||
except:
|
||||
pass
|
||||
|
||||
# string with NaN-like
|
||||
try:
|
||||
mask = ~algorithms.isin(arg, list(tslib.nat_strings))
|
||||
return calc_with_mask(arg, mask)
|
||||
except:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Fixed time formats for time parsing
|
||||
_time_formats = ["%H:%M", "%H%M", "%I:%M%p", "%I%M%p",
|
||||
"%H:%M:%S", "%H%M%S", "%I:%M:%S%p", "%I%M%S%p"]
|
||||
|
||||
|
||||
def _guess_time_format_for_array(arr):
|
||||
# Try to guess the format based on the first non-NaN element
|
||||
non_nan_elements = notna(arr).nonzero()[0]
|
||||
if len(non_nan_elements):
|
||||
element = arr[non_nan_elements[0]]
|
||||
for time_format in _time_formats:
|
||||
try:
|
||||
datetime.strptime(element, time_format)
|
||||
return time_format
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def to_time(arg, format=None, infer_time_format=False, errors='raise'):
|
||||
"""
|
||||
Parse time strings to time objects using fixed strptime formats ("%H:%M",
|
||||
"%H%M", "%I:%M%p", "%I%M%p", "%H:%M:%S", "%H%M%S", "%I:%M:%S%p",
|
||||
"%I%M%S%p")
|
||||
|
||||
Use infer_time_format if all the strings are in the same format to speed
|
||||
up conversion.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arg : string in time format, datetime.time, list, tuple, 1-d array, Series
|
||||
format : str, default None
|
||||
Format used to convert arg into a time object. If None, fixed formats
|
||||
are used.
|
||||
infer_time_format: bool, default False
|
||||
Infer the time format based on the first non-NaN element. If all
|
||||
strings are in the same format, this will speed up conversion.
|
||||
errors : {'ignore', 'raise', 'coerce'}, default 'raise'
|
||||
- If 'raise', then invalid parsing will raise an exception
|
||||
- If 'coerce', then invalid parsing will be set as None
|
||||
- If 'ignore', then invalid parsing will return the input
|
||||
|
||||
Returns
|
||||
-------
|
||||
datetime.time
|
||||
"""
|
||||
from pandas.core.series import Series
|
||||
|
||||
def _convert_listlike(arg, format):
|
||||
|
||||
if isinstance(arg, (list, tuple)):
|
||||
arg = np.array(arg, dtype='O')
|
||||
|
||||
elif getattr(arg, 'ndim', 1) > 1:
|
||||
raise TypeError('arg must be a string, datetime, list, tuple, '
|
||||
'1-d array, or Series')
|
||||
|
||||
arg = _ensure_object(arg)
|
||||
|
||||
if infer_time_format and format is None:
|
||||
format = _guess_time_format_for_array(arg)
|
||||
|
||||
times = []
|
||||
if format is not None:
|
||||
for element in arg:
|
||||
try:
|
||||
times.append(datetime.strptime(element, format).time())
|
||||
except (ValueError, TypeError):
|
||||
if errors == 'raise':
|
||||
msg = ("Cannot convert {element} to a time with given "
|
||||
"format {format}").format(element=element,
|
||||
format=format)
|
||||
raise ValueError(msg)
|
||||
elif errors == 'ignore':
|
||||
return arg
|
||||
else:
|
||||
times.append(None)
|
||||
else:
|
||||
formats = _time_formats[:]
|
||||
format_found = False
|
||||
for element in arg:
|
||||
time_object = None
|
||||
for time_format in formats:
|
||||
try:
|
||||
time_object = datetime.strptime(element,
|
||||
time_format).time()
|
||||
if not format_found:
|
||||
# Put the found format in front
|
||||
fmt = formats.pop(formats.index(time_format))
|
||||
formats.insert(0, fmt)
|
||||
format_found = True
|
||||
break
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
if time_object is not None:
|
||||
times.append(time_object)
|
||||
elif errors == 'raise':
|
||||
raise ValueError("Cannot convert arg {arg} to "
|
||||
"a time".format(arg=arg))
|
||||
elif errors == 'ignore':
|
||||
return arg
|
||||
else:
|
||||
times.append(None)
|
||||
|
||||
return times
|
||||
|
||||
if arg is None:
|
||||
return arg
|
||||
elif isinstance(arg, time):
|
||||
return arg
|
||||
elif isinstance(arg, Series):
|
||||
values = _convert_listlike(arg._values, format)
|
||||
return Series(values, index=arg.index, name=arg.name)
|
||||
elif isinstance(arg, ABCIndexClass):
|
||||
return _convert_listlike(arg, format)
|
||||
elif is_list_like(arg):
|
||||
return _convert_listlike(arg, format)
|
||||
|
||||
return _convert_listlike(np.array([arg]), format)[0]
|
||||
|
||||
|
||||
def format(dt):
|
||||
"""Returns date in YYYYMMDD format."""
|
||||
return dt.strftime('%Y%m%d')
|
||||
|
||||
|
||||
OLE_TIME_ZERO = datetime(1899, 12, 30, 0, 0, 0)
|
||||
|
||||
|
||||
def ole2datetime(oledt):
|
||||
"""function for converting excel date to normal date format"""
|
||||
val = float(oledt)
|
||||
|
||||
# Excel has a bug where it thinks the date 2/29/1900 exists
|
||||
# we just reject any date before 3/1/1900.
|
||||
if val < 61:
|
||||
msg = "Value is outside of acceptable range: {value}".format(value=val)
|
||||
raise ValueError(msg)
|
||||
|
||||
return OLE_TIME_ZERO + timedelta(days=val)
|
||||
@@ -0,0 +1,177 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from pandas.core.dtypes.common import (
|
||||
is_scalar,
|
||||
is_numeric_dtype,
|
||||
is_decimal,
|
||||
is_datetime_or_timedelta_dtype,
|
||||
is_number,
|
||||
_ensure_object)
|
||||
from pandas.core.dtypes.generic import ABCSeries, ABCIndexClass
|
||||
from pandas.core.dtypes.cast import maybe_downcast_to_dtype
|
||||
from pandas._libs import lib
|
||||
|
||||
|
||||
def to_numeric(arg, errors='raise', downcast=None):
|
||||
"""
|
||||
Convert argument to a numeric type.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arg : list, tuple, 1-d array, or Series
|
||||
errors : {'ignore', 'raise', 'coerce'}, default 'raise'
|
||||
- If 'raise', then invalid parsing will raise an exception
|
||||
- If 'coerce', then invalid parsing will be set as NaN
|
||||
- If 'ignore', then invalid parsing will return the input
|
||||
downcast : {'integer', 'signed', 'unsigned', 'float'} , default None
|
||||
If not None, and if the data has been successfully cast to a
|
||||
numerical dtype (or if the data was numeric to begin with),
|
||||
downcast that resulting data to the smallest numerical dtype
|
||||
possible according to the following rules:
|
||||
|
||||
- 'integer' or 'signed': smallest signed int dtype (min.: np.int8)
|
||||
- 'unsigned': smallest unsigned int dtype (min.: np.uint8)
|
||||
- 'float': smallest float dtype (min.: np.float32)
|
||||
|
||||
As this behaviour is separate from the core conversion to
|
||||
numeric values, any errors raised during the downcasting
|
||||
will be surfaced regardless of the value of the 'errors' input.
|
||||
|
||||
In addition, downcasting will only occur if the size
|
||||
of the resulting data's dtype is strictly larger than
|
||||
the dtype it is to be cast to, so if none of the dtypes
|
||||
checked satisfy that specification, no downcasting will be
|
||||
performed on the data.
|
||||
|
||||
.. versionadded:: 0.19.0
|
||||
|
||||
Returns
|
||||
-------
|
||||
ret : numeric if parsing succeeded.
|
||||
Return type depends on input. Series if Series, otherwise ndarray
|
||||
|
||||
Examples
|
||||
--------
|
||||
Take separate series and convert to numeric, coercing when told to
|
||||
|
||||
>>> import pandas as pd
|
||||
>>> s = pd.Series(['1.0', '2', -3])
|
||||
>>> pd.to_numeric(s)
|
||||
0 1.0
|
||||
1 2.0
|
||||
2 -3.0
|
||||
dtype: float64
|
||||
>>> pd.to_numeric(s, downcast='float')
|
||||
0 1.0
|
||||
1 2.0
|
||||
2 -3.0
|
||||
dtype: float32
|
||||
>>> pd.to_numeric(s, downcast='signed')
|
||||
0 1
|
||||
1 2
|
||||
2 -3
|
||||
dtype: int8
|
||||
>>> s = pd.Series(['apple', '1.0', '2', -3])
|
||||
>>> pd.to_numeric(s, errors='ignore')
|
||||
0 apple
|
||||
1 1.0
|
||||
2 2
|
||||
3 -3
|
||||
dtype: object
|
||||
>>> pd.to_numeric(s, errors='coerce')
|
||||
0 NaN
|
||||
1 1.0
|
||||
2 2.0
|
||||
3 -3.0
|
||||
dtype: float64
|
||||
|
||||
See also
|
||||
--------
|
||||
pandas.DataFrame.astype : Cast argument to a specified dtype.
|
||||
pandas.to_datetime : Convert argument to datetime.
|
||||
pandas.to_timedelta : Convert argument to timedelta.
|
||||
numpy.ndarray.astype : Cast a numpy array to a specified type.
|
||||
"""
|
||||
if downcast not in (None, 'integer', 'signed', 'unsigned', 'float'):
|
||||
raise ValueError('invalid downcasting method provided')
|
||||
|
||||
is_series = False
|
||||
is_index = False
|
||||
is_scalars = False
|
||||
|
||||
if isinstance(arg, ABCSeries):
|
||||
is_series = True
|
||||
values = arg.values
|
||||
elif isinstance(arg, ABCIndexClass):
|
||||
is_index = True
|
||||
values = arg.asi8
|
||||
if values is None:
|
||||
values = arg.values
|
||||
elif isinstance(arg, (list, tuple)):
|
||||
values = np.array(arg, dtype='O')
|
||||
elif is_scalar(arg):
|
||||
if is_decimal(arg):
|
||||
return float(arg)
|
||||
if is_number(arg):
|
||||
return arg
|
||||
is_scalars = True
|
||||
values = np.array([arg], dtype='O')
|
||||
elif getattr(arg, 'ndim', 1) > 1:
|
||||
raise TypeError('arg must be a list, tuple, 1-d array, or Series')
|
||||
else:
|
||||
values = arg
|
||||
|
||||
try:
|
||||
if is_numeric_dtype(values):
|
||||
pass
|
||||
elif is_datetime_or_timedelta_dtype(values):
|
||||
values = values.astype(np.int64)
|
||||
else:
|
||||
values = _ensure_object(values)
|
||||
coerce_numeric = False if errors in ('ignore', 'raise') else True
|
||||
values = lib.maybe_convert_numeric(values, set(),
|
||||
coerce_numeric=coerce_numeric)
|
||||
|
||||
except Exception:
|
||||
if errors == 'raise':
|
||||
raise
|
||||
|
||||
# attempt downcast only if the data has been successfully converted
|
||||
# to a numerical dtype and if a downcast method has been specified
|
||||
if downcast is not None and is_numeric_dtype(values):
|
||||
typecodes = None
|
||||
|
||||
if downcast in ('integer', 'signed'):
|
||||
typecodes = np.typecodes['Integer']
|
||||
elif downcast == 'unsigned' and np.min(values) >= 0:
|
||||
typecodes = np.typecodes['UnsignedInteger']
|
||||
elif downcast == 'float':
|
||||
typecodes = np.typecodes['Float']
|
||||
|
||||
# pandas support goes only to np.float32,
|
||||
# as float dtypes smaller than that are
|
||||
# extremely rare and not well supported
|
||||
float_32_char = np.dtype(np.float32).char
|
||||
float_32_ind = typecodes.index(float_32_char)
|
||||
typecodes = typecodes[float_32_ind:]
|
||||
|
||||
if typecodes is not None:
|
||||
# from smallest to largest
|
||||
for dtype in typecodes:
|
||||
if np.dtype(dtype).itemsize <= values.dtype.itemsize:
|
||||
values = maybe_downcast_to_dtype(values, dtype)
|
||||
|
||||
# successful conversion
|
||||
if values.dtype == dtype:
|
||||
break
|
||||
|
||||
if is_series:
|
||||
return pd.Series(values, index=arg.index, name=arg.name)
|
||||
elif is_index:
|
||||
# because we want to coerce to numeric if possible,
|
||||
# do not use _shallow_copy_with_infer
|
||||
return pd.Index(values, name=arg.name)
|
||||
elif is_scalars:
|
||||
return values[0]
|
||||
else:
|
||||
return values
|
||||
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
timedelta support tools
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pandas._libs.tslib as tslib
|
||||
from pandas._libs.tslibs.timedeltas import (convert_to_timedelta64,
|
||||
array_to_timedelta64)
|
||||
|
||||
from pandas.core.dtypes.common import (
|
||||
_ensure_object,
|
||||
is_integer_dtype,
|
||||
is_timedelta64_dtype,
|
||||
is_list_like)
|
||||
from pandas.core.dtypes.generic import ABCSeries, ABCIndexClass
|
||||
|
||||
|
||||
def to_timedelta(arg, unit='ns', box=True, errors='raise'):
|
||||
"""
|
||||
Convert argument to timedelta
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arg : string, timedelta, list, tuple, 1-d array, or Series
|
||||
unit : unit of the arg (D,h,m,s,ms,us,ns) denote the unit, which is an
|
||||
integer/float number
|
||||
box : boolean, default True
|
||||
- If True returns a Timedelta/TimedeltaIndex of the results
|
||||
- if False returns a np.timedelta64 or ndarray of values of dtype
|
||||
timedelta64[ns]
|
||||
errors : {'ignore', 'raise', 'coerce'}, default 'raise'
|
||||
- If 'raise', then invalid parsing will raise an exception
|
||||
- If 'coerce', then invalid parsing will be set as NaT
|
||||
- If 'ignore', then invalid parsing will return the input
|
||||
|
||||
Returns
|
||||
-------
|
||||
ret : timedelta64/arrays of timedelta64 if parsing succeeded
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Parsing a single string to a Timedelta:
|
||||
|
||||
>>> pd.to_timedelta('1 days 06:05:01.00003')
|
||||
Timedelta('1 days 06:05:01.000030')
|
||||
>>> pd.to_timedelta('15.5us')
|
||||
Timedelta('0 days 00:00:00.000015')
|
||||
|
||||
Parsing a list or array of strings:
|
||||
|
||||
>>> pd.to_timedelta(['1 days 06:05:01.00003', '15.5us', 'nan'])
|
||||
TimedeltaIndex(['1 days 06:05:01.000030', '0 days 00:00:00.000015', NaT],
|
||||
dtype='timedelta64[ns]', freq=None)
|
||||
|
||||
Converting numbers by specifying the `unit` keyword argument:
|
||||
|
||||
>>> pd.to_timedelta(np.arange(5), unit='s')
|
||||
TimedeltaIndex(['00:00:00', '00:00:01', '00:00:02',
|
||||
'00:00:03', '00:00:04'],
|
||||
dtype='timedelta64[ns]', freq=None)
|
||||
>>> pd.to_timedelta(np.arange(5), unit='d')
|
||||
TimedeltaIndex(['0 days', '1 days', '2 days', '3 days', '4 days'],
|
||||
dtype='timedelta64[ns]', freq=None)
|
||||
|
||||
See also
|
||||
--------
|
||||
pandas.DataFrame.astype : Cast argument to a specified dtype.
|
||||
pandas.to_datetime : Convert argument to datetime.
|
||||
"""
|
||||
unit = _validate_timedelta_unit(unit)
|
||||
|
||||
if errors not in ('ignore', 'raise', 'coerce'):
|
||||
raise ValueError("errors must be one of 'ignore', "
|
||||
"'raise', or 'coerce'}")
|
||||
|
||||
if arg is None:
|
||||
return arg
|
||||
elif isinstance(arg, ABCSeries):
|
||||
from pandas import Series
|
||||
values = _convert_listlike(arg._values, unit=unit,
|
||||
box=False, errors=errors)
|
||||
return Series(values, index=arg.index, name=arg.name)
|
||||
elif isinstance(arg, ABCIndexClass):
|
||||
return _convert_listlike(arg, unit=unit, box=box,
|
||||
errors=errors, name=arg.name)
|
||||
elif is_list_like(arg) and getattr(arg, 'ndim', 1) == 0:
|
||||
# extract array scalar and process below
|
||||
arg = arg.item()
|
||||
elif is_list_like(arg) and getattr(arg, 'ndim', 1) == 1:
|
||||
return _convert_listlike(arg, unit=unit, box=box, errors=errors)
|
||||
elif getattr(arg, 'ndim', 1) > 1:
|
||||
raise TypeError('arg must be a string, timedelta, list, tuple, '
|
||||
'1-d array, or Series')
|
||||
|
||||
# ...so it must be a scalar value. Return scalar.
|
||||
return _coerce_scalar_to_timedelta_type(arg, unit=unit,
|
||||
box=box, errors=errors)
|
||||
|
||||
|
||||
_unit_map = {
|
||||
'Y': 'Y',
|
||||
'y': 'Y',
|
||||
'W': 'W',
|
||||
'w': 'W',
|
||||
'D': 'D',
|
||||
'd': 'D',
|
||||
'days': 'D',
|
||||
'Days': 'D',
|
||||
'day': 'D',
|
||||
'Day': 'D',
|
||||
'M': 'M',
|
||||
'H': 'h',
|
||||
'h': 'h',
|
||||
'm': 'm',
|
||||
'T': 'm',
|
||||
'S': 's',
|
||||
's': 's',
|
||||
'L': 'ms',
|
||||
'MS': 'ms',
|
||||
'ms': 'ms',
|
||||
'US': 'us',
|
||||
'us': 'us',
|
||||
'NS': 'ns',
|
||||
'ns': 'ns',
|
||||
}
|
||||
|
||||
|
||||
def _validate_timedelta_unit(arg):
|
||||
""" provide validation / translation for timedelta short units """
|
||||
try:
|
||||
return _unit_map[arg]
|
||||
except:
|
||||
if arg is None:
|
||||
return 'ns'
|
||||
raise ValueError("invalid timedelta unit {arg} provided"
|
||||
.format(arg=arg))
|
||||
|
||||
|
||||
def _coerce_scalar_to_timedelta_type(r, unit='ns', box=True, errors='raise'):
|
||||
"""Convert string 'r' to a timedelta object."""
|
||||
|
||||
try:
|
||||
result = convert_to_timedelta64(r, unit)
|
||||
except ValueError:
|
||||
if errors == 'raise':
|
||||
raise
|
||||
elif errors == 'ignore':
|
||||
return r
|
||||
|
||||
# coerce
|
||||
result = pd.NaT
|
||||
|
||||
if box:
|
||||
result = tslib.Timedelta(result)
|
||||
return result
|
||||
|
||||
|
||||
def _convert_listlike(arg, unit='ns', box=True, errors='raise', name=None):
|
||||
"""Convert a list of objects to a timedelta index object."""
|
||||
|
||||
if isinstance(arg, (list, tuple)) or not hasattr(arg, 'dtype'):
|
||||
arg = np.array(list(arg), dtype='O')
|
||||
|
||||
# these are shortcut-able
|
||||
if is_timedelta64_dtype(arg):
|
||||
value = arg.astype('timedelta64[ns]')
|
||||
elif is_integer_dtype(arg):
|
||||
value = arg.astype('timedelta64[{unit}]'.format(unit=unit)).astype(
|
||||
'timedelta64[ns]', copy=False)
|
||||
else:
|
||||
try:
|
||||
value = array_to_timedelta64(_ensure_object(arg),
|
||||
unit=unit, errors=errors)
|
||||
value = value.astype('timedelta64[ns]', copy=False)
|
||||
except ValueError:
|
||||
if errors == 'ignore':
|
||||
return arg
|
||||
else:
|
||||
# This else-block accounts for the cases when errors='raise'
|
||||
# and errors='coerce'. If errors == 'raise', these errors
|
||||
# should be raised. If errors == 'coerce', we shouldn't
|
||||
# expect any errors to be raised, since all parsing errors
|
||||
# cause coercion to pd.NaT. However, if an error / bug is
|
||||
# introduced that causes an Exception to be raised, we would
|
||||
# like to surface it.
|
||||
raise
|
||||
|
||||
if box:
|
||||
from pandas import TimedeltaIndex
|
||||
value = TimedeltaIndex(value, unit='ns', name=name)
|
||||
return value
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,332 @@
|
||||
"""
|
||||
data hash pandas / numpy objects
|
||||
"""
|
||||
import itertools
|
||||
|
||||
import numpy as np
|
||||
from pandas._libs import hashing, tslib
|
||||
from pandas.core.dtypes.generic import (
|
||||
ABCMultiIndex,
|
||||
ABCIndexClass,
|
||||
ABCSeries,
|
||||
ABCDataFrame)
|
||||
from pandas.core.dtypes.common import (
|
||||
is_categorical_dtype, is_list_like)
|
||||
from pandas.core.dtypes.missing import isna
|
||||
from pandas.core.dtypes.cast import infer_dtype_from_scalar
|
||||
|
||||
|
||||
# 16 byte long hashing key
|
||||
_default_hash_key = '0123456789123456'
|
||||
|
||||
|
||||
def _combine_hash_arrays(arrays, num_items):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
arrays : generator
|
||||
num_items : int
|
||||
|
||||
Should be the same as CPython's tupleobject.c
|
||||
"""
|
||||
try:
|
||||
first = next(arrays)
|
||||
except StopIteration:
|
||||
return np.array([], dtype=np.uint64)
|
||||
|
||||
arrays = itertools.chain([first], arrays)
|
||||
|
||||
mult = np.uint64(1000003)
|
||||
out = np.zeros_like(first) + np.uint64(0x345678)
|
||||
for i, a in enumerate(arrays):
|
||||
inverse_i = num_items - i
|
||||
out ^= a
|
||||
out *= mult
|
||||
mult += np.uint64(82520 + inverse_i + inverse_i)
|
||||
assert i + 1 == num_items, 'Fed in wrong num_items'
|
||||
out += np.uint64(97531)
|
||||
return out
|
||||
|
||||
|
||||
def hash_pandas_object(obj, index=True, encoding='utf8', hash_key=None,
|
||||
categorize=True):
|
||||
"""
|
||||
Return a data hash of the Index/Series/DataFrame
|
||||
|
||||
.. versionadded:: 0.19.2
|
||||
|
||||
Parameters
|
||||
----------
|
||||
index : boolean, default True
|
||||
include the index in the hash (if Series/DataFrame)
|
||||
encoding : string, default 'utf8'
|
||||
encoding for data & key when strings
|
||||
hash_key : string key to encode, default to _default_hash_key
|
||||
categorize : bool, default True
|
||||
Whether to first categorize object arrays before hashing. This is more
|
||||
efficient when the array contains duplicate values.
|
||||
|
||||
.. versionadded:: 0.20.0
|
||||
|
||||
Returns
|
||||
-------
|
||||
Series of uint64, same length as the object
|
||||
|
||||
"""
|
||||
from pandas import Series
|
||||
if hash_key is None:
|
||||
hash_key = _default_hash_key
|
||||
|
||||
if isinstance(obj, ABCMultiIndex):
|
||||
return Series(hash_tuples(obj, encoding, hash_key),
|
||||
dtype='uint64', copy=False)
|
||||
|
||||
if isinstance(obj, ABCIndexClass):
|
||||
h = hash_array(obj.values, encoding, hash_key,
|
||||
categorize).astype('uint64', copy=False)
|
||||
h = Series(h, index=obj, dtype='uint64', copy=False)
|
||||
elif isinstance(obj, ABCSeries):
|
||||
h = hash_array(obj.values, encoding, hash_key,
|
||||
categorize).astype('uint64', copy=False)
|
||||
if index:
|
||||
index_iter = (hash_pandas_object(obj.index,
|
||||
index=False,
|
||||
encoding=encoding,
|
||||
hash_key=hash_key,
|
||||
categorize=categorize).values
|
||||
for _ in [None])
|
||||
arrays = itertools.chain([h], index_iter)
|
||||
h = _combine_hash_arrays(arrays, 2)
|
||||
|
||||
h = Series(h, index=obj.index, dtype='uint64', copy=False)
|
||||
|
||||
elif isinstance(obj, ABCDataFrame):
|
||||
hashes = (hash_array(series.values) for _, series in obj.iteritems())
|
||||
num_items = len(obj.columns)
|
||||
if index:
|
||||
index_hash_generator = (hash_pandas_object(obj.index,
|
||||
index=False,
|
||||
encoding=encoding,
|
||||
hash_key=hash_key,
|
||||
categorize=categorize).values # noqa
|
||||
for _ in [None])
|
||||
num_items += 1
|
||||
hashes = itertools.chain(hashes, index_hash_generator)
|
||||
h = _combine_hash_arrays(hashes, num_items)
|
||||
|
||||
h = Series(h, index=obj.index, dtype='uint64', copy=False)
|
||||
else:
|
||||
raise TypeError("Unexpected type for hashing %s" % type(obj))
|
||||
return h
|
||||
|
||||
|
||||
def hash_tuples(vals, encoding='utf8', hash_key=None):
|
||||
"""
|
||||
Hash an MultiIndex / list-of-tuples efficiently
|
||||
|
||||
.. versionadded:: 0.20.0
|
||||
|
||||
Parameters
|
||||
----------
|
||||
vals : MultiIndex, list-of-tuples, or single tuple
|
||||
encoding : string, default 'utf8'
|
||||
hash_key : string key to encode, default to _default_hash_key
|
||||
|
||||
Returns
|
||||
-------
|
||||
ndarray of hashed values array
|
||||
"""
|
||||
is_tuple = False
|
||||
if isinstance(vals, tuple):
|
||||
vals = [vals]
|
||||
is_tuple = True
|
||||
elif not is_list_like(vals):
|
||||
raise TypeError("must be convertible to a list-of-tuples")
|
||||
|
||||
from pandas import Categorical, MultiIndex
|
||||
|
||||
if not isinstance(vals, ABCMultiIndex):
|
||||
vals = MultiIndex.from_tuples(vals)
|
||||
|
||||
# create a list-of-Categoricals
|
||||
vals = [Categorical(vals.labels[level],
|
||||
vals.levels[level],
|
||||
ordered=False,
|
||||
fastpath=True)
|
||||
for level in range(vals.nlevels)]
|
||||
|
||||
# hash the list-of-ndarrays
|
||||
hashes = (_hash_categorical(cat,
|
||||
encoding=encoding,
|
||||
hash_key=hash_key)
|
||||
for cat in vals)
|
||||
h = _combine_hash_arrays(hashes, len(vals))
|
||||
if is_tuple:
|
||||
h = h[0]
|
||||
|
||||
return h
|
||||
|
||||
|
||||
def hash_tuple(val, encoding='utf8', hash_key=None):
|
||||
"""
|
||||
Hash a single tuple efficiently
|
||||
|
||||
Parameters
|
||||
----------
|
||||
val : single tuple
|
||||
encoding : string, default 'utf8'
|
||||
hash_key : string key to encode, default to _default_hash_key
|
||||
|
||||
Returns
|
||||
-------
|
||||
hash
|
||||
|
||||
"""
|
||||
hashes = (_hash_scalar(v, encoding=encoding, hash_key=hash_key)
|
||||
for v in val)
|
||||
|
||||
h = _combine_hash_arrays(hashes, len(val))[0]
|
||||
|
||||
return h
|
||||
|
||||
|
||||
def _hash_categorical(c, encoding, hash_key):
|
||||
"""
|
||||
Hash a Categorical by hashing its categories, and then mapping the codes
|
||||
to the hashes
|
||||
|
||||
Parameters
|
||||
----------
|
||||
c : Categorical
|
||||
encoding : string, default 'utf8'
|
||||
hash_key : string key to encode, default to _default_hash_key
|
||||
|
||||
Returns
|
||||
-------
|
||||
ndarray of hashed values array, same size as len(c)
|
||||
"""
|
||||
hashed = hash_array(c.categories.values, encoding, hash_key,
|
||||
categorize=False)
|
||||
|
||||
# we have uint64, as we don't directly support missing values
|
||||
# we don't want to use take_nd which will coerce to float
|
||||
# instead, directly construct the result with a
|
||||
# max(np.uint64) as the missing value indicator
|
||||
#
|
||||
# TODO: GH 15362
|
||||
|
||||
mask = c.isna()
|
||||
if len(hashed):
|
||||
result = hashed.take(c.codes)
|
||||
else:
|
||||
result = np.zeros(len(mask), dtype='uint64')
|
||||
|
||||
if mask.any():
|
||||
result[mask] = np.iinfo(np.uint64).max
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def hash_array(vals, encoding='utf8', hash_key=None, categorize=True):
|
||||
"""
|
||||
Given a 1d array, return an array of deterministic integers.
|
||||
|
||||
.. versionadded:: 0.19.2
|
||||
|
||||
Parameters
|
||||
----------
|
||||
vals : ndarray, Categorical
|
||||
encoding : string, default 'utf8'
|
||||
encoding for data & key when strings
|
||||
hash_key : string key to encode, default to _default_hash_key
|
||||
categorize : bool, default True
|
||||
Whether to first categorize object arrays before hashing. This is more
|
||||
efficient when the array contains duplicate values.
|
||||
|
||||
.. versionadded:: 0.20.0
|
||||
|
||||
Returns
|
||||
-------
|
||||
1d uint64 numpy array of hash values, same length as the vals
|
||||
|
||||
"""
|
||||
|
||||
if not hasattr(vals, 'dtype'):
|
||||
raise TypeError("must pass a ndarray-like")
|
||||
dtype = vals.dtype
|
||||
|
||||
if hash_key is None:
|
||||
hash_key = _default_hash_key
|
||||
|
||||
# For categoricals, we hash the categories, then remap the codes to the
|
||||
# hash values. (This check is above the complex check so that we don't ask
|
||||
# numpy if categorical is a subdtype of complex, as it will choke).
|
||||
if is_categorical_dtype(dtype):
|
||||
return _hash_categorical(vals, encoding, hash_key)
|
||||
|
||||
# we'll be working with everything as 64-bit values, so handle this
|
||||
# 128-bit value early
|
||||
elif np.issubdtype(dtype, np.complex128):
|
||||
return hash_array(vals.real) + 23 * hash_array(vals.imag)
|
||||
|
||||
# First, turn whatever array this is into unsigned 64-bit ints, if we can
|
||||
# manage it.
|
||||
elif isinstance(dtype, np.bool):
|
||||
vals = vals.astype('u8')
|
||||
elif issubclass(dtype.type, (np.datetime64, np.timedelta64)):
|
||||
vals = vals.view('i8').astype('u8', copy=False)
|
||||
elif issubclass(dtype.type, np.number) and dtype.itemsize <= 8:
|
||||
vals = vals.view('u{}'.format(vals.dtype.itemsize)).astype('u8')
|
||||
else:
|
||||
# With repeated values, its MUCH faster to categorize object dtypes,
|
||||
# then hash and rename categories. We allow skipping the categorization
|
||||
# when the values are known/likely to be unique.
|
||||
if categorize:
|
||||
from pandas import factorize, Categorical, Index
|
||||
codes, categories = factorize(vals, sort=False)
|
||||
cat = Categorical(codes, Index(categories),
|
||||
ordered=False, fastpath=True)
|
||||
return _hash_categorical(cat, encoding, hash_key)
|
||||
|
||||
try:
|
||||
vals = hashing.hash_object_array(vals, hash_key, encoding)
|
||||
except TypeError:
|
||||
# we have mixed types
|
||||
vals = hashing.hash_object_array(vals.astype(str).astype(object),
|
||||
hash_key, encoding)
|
||||
|
||||
# Then, redistribute these 64-bit ints within the space of 64-bit ints
|
||||
vals ^= vals >> 30
|
||||
vals *= np.uint64(0xbf58476d1ce4e5b9)
|
||||
vals ^= vals >> 27
|
||||
vals *= np.uint64(0x94d049bb133111eb)
|
||||
vals ^= vals >> 31
|
||||
return vals
|
||||
|
||||
|
||||
def _hash_scalar(val, encoding='utf8', hash_key=None):
|
||||
"""
|
||||
Hash scalar value
|
||||
|
||||
Returns
|
||||
-------
|
||||
1d uint64 numpy array of hash value, of length 1
|
||||
"""
|
||||
|
||||
if isna(val):
|
||||
# this is to be consistent with the _hash_categorical implementation
|
||||
return np.array([np.iinfo(np.uint64).max], dtype='u8')
|
||||
|
||||
if getattr(val, 'tzinfo', None) is not None:
|
||||
# for tz-aware datetimes, we need the underlying naive UTC value and
|
||||
# not the tz aware object or pd extension type (as
|
||||
# infer_dtype_from_scalar would do)
|
||||
if not isinstance(val, tslib.Timestamp):
|
||||
val = tslib.Timestamp(val)
|
||||
val = val.tz_convert(None)
|
||||
|
||||
dtype, val = infer_dtype_from_scalar(val)
|
||||
vals = np.array([val], dtype=dtype)
|
||||
|
||||
return hash_array(vals, hash_key=hash_key, encoding=encoding,
|
||||
categorize=False)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,184 @@
|
||||
# flake8: noqa
|
||||
|
||||
"""
|
||||
Expose public exceptions & warnings
|
||||
"""
|
||||
|
||||
from pandas._libs.tslib import OutOfBoundsDatetime
|
||||
|
||||
|
||||
class PerformanceWarning(Warning):
|
||||
"""
|
||||
Warning raised when there is a possible
|
||||
performance impact.
|
||||
"""
|
||||
|
||||
class UnsupportedFunctionCall(ValueError):
|
||||
"""
|
||||
Exception raised when attempting to call a numpy function
|
||||
on a pandas object, but that function is not supported by
|
||||
the object e.g. ``np.cumsum(groupby_object)``.
|
||||
"""
|
||||
|
||||
class UnsortedIndexError(KeyError):
|
||||
"""
|
||||
Error raised when attempting to get a slice of a MultiIndex,
|
||||
and the index has not been lexsorted. Subclass of `KeyError`.
|
||||
|
||||
.. versionadded:: 0.20.0
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class ParserError(ValueError):
|
||||
"""
|
||||
Exception that is raised by an error encountered in `pd.read_csv`.
|
||||
"""
|
||||
|
||||
|
||||
class DtypeWarning(Warning):
|
||||
"""
|
||||
Warning raised when reading different dtypes in a column from a file.
|
||||
|
||||
Raised for a dtype incompatibility. This can happen whenever `read_csv`
|
||||
or `read_table` encounter non-uniform dtypes in a column(s) of a given
|
||||
CSV file.
|
||||
|
||||
See Also
|
||||
--------
|
||||
pandas.read_csv : Read CSV (comma-separated) file into a DataFrame.
|
||||
pandas.read_table : Read general delimited file into a DataFrame.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This warning is issued when dealing with larger files because the dtype
|
||||
checking happens per chunk read.
|
||||
|
||||
Despite the warning, the CSV file is read with mixed types in a single
|
||||
column which will be an object type. See the examples below to better
|
||||
understand this issue.
|
||||
|
||||
Examples
|
||||
--------
|
||||
This example creates and reads a large CSV file with a column that contains
|
||||
`int` and `str`.
|
||||
|
||||
>>> df = pd.DataFrame({'a': (['1'] * 100000 + ['X'] * 100000 +
|
||||
... ['1'] * 100000),
|
||||
... 'b': ['b'] * 300000})
|
||||
>>> df.to_csv('test.csv', index=False)
|
||||
>>> df2 = pd.read_csv('test.csv')
|
||||
... # DtypeWarning: Columns (0) have mixed types
|
||||
|
||||
Important to notice that ``df2`` will contain both `str` and `int` for the
|
||||
same input, '1'.
|
||||
|
||||
>>> df2.iloc[262140, 0]
|
||||
'1'
|
||||
>>> type(df2.iloc[262140, 0])
|
||||
<class 'str'>
|
||||
>>> df2.iloc[262150, 0]
|
||||
1
|
||||
>>> type(df2.iloc[262150, 0])
|
||||
<class 'int'>
|
||||
|
||||
One way to solve this issue is using the `dtype` parameter in the
|
||||
`read_csv` and `read_table` functions to explicit the conversion:
|
||||
|
||||
>>> df2 = pd.read_csv('test.csv', sep=',', dtype={'a': str})
|
||||
|
||||
No warning was issued.
|
||||
|
||||
>>> import os
|
||||
>>> os.remove('test.csv')
|
||||
"""
|
||||
|
||||
|
||||
class EmptyDataError(ValueError):
|
||||
"""
|
||||
Exception that is thrown in `pd.read_csv` (by both the C and
|
||||
Python engines) when empty data or header is encountered.
|
||||
"""
|
||||
|
||||
|
||||
class ParserWarning(Warning):
|
||||
"""
|
||||
Warning raised when reading a file that doesn't use the default 'c' parser.
|
||||
|
||||
Raised by `pd.read_csv` and `pd.read_table` when it is necessary to change
|
||||
parsers, generally from the default 'c' parser to 'python'.
|
||||
|
||||
It happens due to a lack of support or functionality for parsing a
|
||||
particular attribute of a CSV file with the requested engine.
|
||||
|
||||
Currently, 'c' unsupported options include the following parameters:
|
||||
|
||||
1. `sep` other than a single character (e.g. regex separators)
|
||||
2. `skipfooter` higher than 0
|
||||
3. `sep=None` with `delim_whitespace=False`
|
||||
|
||||
The warning can be avoided by adding `engine='python'` as a parameter in
|
||||
`pd.read_csv` and `pd.read_table` methods.
|
||||
|
||||
See Also
|
||||
--------
|
||||
pd.read_csv : Read CSV (comma-separated) file into DataFrame.
|
||||
pd.read_table : Read general delimited file into DataFrame.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Using a `sep` in `pd.read_csv` other than a single character:
|
||||
|
||||
>>> import io
|
||||
>>> csv = u'''a;b;c
|
||||
... 1;1,8
|
||||
... 1;2,1'''
|
||||
>>> df = pd.read_csv(io.StringIO(csv), sep='[;,]')
|
||||
... # ParserWarning: Falling back to the 'python' engine...
|
||||
|
||||
Adding `engine='python'` to `pd.read_csv` removes the Warning:
|
||||
|
||||
>>> df = pd.read_csv(io.StringIO(csv), sep='[;,]', engine='python')
|
||||
"""
|
||||
|
||||
|
||||
class MergeError(ValueError):
|
||||
"""
|
||||
Error raised when problems arise during merging due to problems
|
||||
with input data. Subclass of `ValueError`.
|
||||
"""
|
||||
|
||||
|
||||
class NullFrequencyError(ValueError):
|
||||
"""
|
||||
Error raised when a null `freq` attribute is used in an operation
|
||||
that needs a non-null frequency, particularly `DatetimeIndex.shift`,
|
||||
`TimedeltaIndex.shift`, `PeriodIndex.shift`.
|
||||
"""
|
||||
|
||||
|
||||
class AccessorRegistrationWarning(Warning):
|
||||
"""Warning for attribute conflicts in accessor registration."""
|
||||
|
||||
|
||||
class AbstractMethodError(NotImplementedError):
|
||||
"""Raise this error instead of NotImplementedError for abstract methods
|
||||
while keeping compatibility with Python 2 and Python 3.
|
||||
"""
|
||||
|
||||
def __init__(self, class_instance, methodtype='method'):
|
||||
types = {'method', 'classmethod', 'staticmethod', 'property'}
|
||||
if methodtype not in types:
|
||||
msg = 'methodtype must be one of {}, got {} instead.'.format(
|
||||
methodtype, types)
|
||||
raise ValueError(msg)
|
||||
self.methodtype = methodtype
|
||||
self.class_instance = class_instance
|
||||
|
||||
def __str__(self):
|
||||
if self.methodtype == 'classmethod':
|
||||
name = self.class_instance.__name__
|
||||
else:
|
||||
name = self.class_instance.__class__.__name__
|
||||
msg = "This {methodtype} must be defined in the concrete class {name}"
|
||||
return (msg.format(methodtype=self.methodtype, name=name))
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
import warnings
|
||||
|
||||
warnings.warn("Styler has been moved from pandas.formats.style.Styler"
|
||||
" to pandas.io.formats.style.Styler. This shim will be"
|
||||
" removed in pandas 0.21",
|
||||
FutureWarning)
|
||||
from pandas.io.formats.style import Styler # noqa
|
||||
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
Data IO api
|
||||
"""
|
||||
|
||||
# flake8: noqa
|
||||
|
||||
from pandas.io.parsers import read_csv, read_table, read_fwf
|
||||
from pandas.io.clipboards import read_clipboard
|
||||
from pandas.io.excel import ExcelFile, ExcelWriter, read_excel
|
||||
from pandas.io.pytables import HDFStore, get_store, read_hdf
|
||||
from pandas.io.json import read_json
|
||||
from pandas.io.html import read_html
|
||||
from pandas.io.sql import read_sql, read_sql_table, read_sql_query
|
||||
from pandas.io.sas import read_sas
|
||||
from pandas.io.feather_format import read_feather
|
||||
from pandas.io.parquet import read_parquet
|
||||
from pandas.io.stata import read_stata
|
||||
from pandas.io.pickle import read_pickle, to_pickle
|
||||
from pandas.io.packers import read_msgpack, to_msgpack
|
||||
from pandas.io.gbq import read_gbq
|
||||
|
||||
# deprecation, xref #13790
|
||||
def Term(*args, **kwargs):
|
||||
import warnings
|
||||
|
||||
warnings.warn("pd.Term is deprecated as it is not "
|
||||
"applicable to user code. Instead use in-line "
|
||||
"string expressions in the where clause when "
|
||||
"searching in HDFStore",
|
||||
FutureWarning, stacklevel=2)
|
||||
from pandas.io.pytables import Term
|
||||
return Term(*args, **kwargs)
|
||||
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
Pyperclip
|
||||
|
||||
A cross-platform clipboard module for Python. (only handles plain text for now)
|
||||
By Al Sweigart al@inventwithpython.com
|
||||
BSD License
|
||||
|
||||
Usage:
|
||||
import pyperclip
|
||||
pyperclip.copy('The text to be copied to the clipboard.')
|
||||
spam = pyperclip.paste()
|
||||
|
||||
if not pyperclip.copy:
|
||||
print("Copy functionality unavailable!")
|
||||
|
||||
On Windows, no additional modules are needed.
|
||||
On Mac, the module uses pbcopy and pbpaste, which should come with the os.
|
||||
On Linux, install xclip or xsel via package manager. For example, in Debian:
|
||||
sudo apt-get install xclip
|
||||
|
||||
Otherwise on Linux, you will need the gtk, qtpy or PyQt modules installed.
|
||||
qtpy also requires a python-qt-bindings module: PyQt4, PyQt5, PySide, PySide2
|
||||
|
||||
gtk and PyQt4 modules are not available for Python 3,
|
||||
and this module does not work with PyGObject yet.
|
||||
"""
|
||||
__version__ = '1.5.27'
|
||||
|
||||
import platform
|
||||
import os
|
||||
import subprocess
|
||||
from .clipboards import (init_osx_clipboard,
|
||||
init_gtk_clipboard, init_qt_clipboard,
|
||||
init_xclip_clipboard, init_xsel_clipboard,
|
||||
init_klipper_clipboard, init_no_clipboard)
|
||||
from .windows import init_windows_clipboard
|
||||
|
||||
# `import qtpy` sys.exit()s if DISPLAY is not in the environment.
|
||||
# Thus, we need to detect the presence of $DISPLAY manually
|
||||
# and not load qtpy if it is absent.
|
||||
HAS_DISPLAY = os.getenv("DISPLAY", False)
|
||||
CHECK_CMD = "where" if platform.system() == "Windows" else "which"
|
||||
|
||||
|
||||
def _executable_exists(name):
|
||||
return subprocess.call([CHECK_CMD, name],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0
|
||||
|
||||
|
||||
def determine_clipboard():
|
||||
# Determine the OS/platform and set
|
||||
# the copy() and paste() functions accordingly.
|
||||
if 'cygwin' in platform.system().lower():
|
||||
# FIXME: pyperclip currently does not support Cygwin,
|
||||
# see https://github.com/asweigart/pyperclip/issues/55
|
||||
pass
|
||||
elif os.name == 'nt' or platform.system() == 'Windows':
|
||||
return init_windows_clipboard()
|
||||
if os.name == 'mac' or platform.system() == 'Darwin':
|
||||
return init_osx_clipboard()
|
||||
if HAS_DISPLAY:
|
||||
# Determine which command/module is installed, if any.
|
||||
try:
|
||||
# Check if gtk is installed
|
||||
import gtk # noqa
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
return init_gtk_clipboard()
|
||||
|
||||
try:
|
||||
# qtpy is a small abstraction layer that lets you write
|
||||
# applications using a single api call to either PyQt or PySide
|
||||
# https://pypi.org/project/QtPy
|
||||
import qtpy # noqa
|
||||
except ImportError:
|
||||
# If qtpy isn't installed, fall back on importing PyQt5, or PyQt5
|
||||
try:
|
||||
import PyQt5 # noqa
|
||||
except ImportError:
|
||||
try:
|
||||
import PyQt4 # noqa
|
||||
except ImportError:
|
||||
pass # fail fast for all non-ImportError exceptions.
|
||||
else:
|
||||
return init_qt_clipboard()
|
||||
else:
|
||||
return init_qt_clipboard()
|
||||
pass
|
||||
else:
|
||||
return init_qt_clipboard()
|
||||
|
||||
if _executable_exists("xclip"):
|
||||
return init_xclip_clipboard()
|
||||
if _executable_exists("xsel"):
|
||||
return init_xsel_clipboard()
|
||||
if _executable_exists("klipper") and _executable_exists("qdbus"):
|
||||
return init_klipper_clipboard()
|
||||
|
||||
return init_no_clipboard()
|
||||
|
||||
|
||||
def set_clipboard(clipboard):
|
||||
global copy, paste
|
||||
|
||||
clipboard_types = {'osx': init_osx_clipboard,
|
||||
'gtk': init_gtk_clipboard,
|
||||
'qt': init_qt_clipboard,
|
||||
'xclip': init_xclip_clipboard,
|
||||
'xsel': init_xsel_clipboard,
|
||||
'klipper': init_klipper_clipboard,
|
||||
'windows': init_windows_clipboard,
|
||||
'no': init_no_clipboard}
|
||||
|
||||
copy, paste = clipboard_types[clipboard]()
|
||||
|
||||
|
||||
copy, paste = determine_clipboard()
|
||||
|
||||
__all__ = ["copy", "paste"]
|
||||
|
||||
|
||||
# pandas aliases
|
||||
clipboard_get = paste
|
||||
clipboard_set = copy
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,143 @@
|
||||
import subprocess
|
||||
from .exceptions import PyperclipException
|
||||
from pandas.compat import PY2, text_type
|
||||
|
||||
EXCEPT_MSG = """
|
||||
Pyperclip could not find a copy/paste mechanism for your system.
|
||||
For more information, please visit https://pyperclip.readthedocs.org """
|
||||
|
||||
|
||||
def init_osx_clipboard():
|
||||
def copy_osx(text):
|
||||
p = subprocess.Popen(['pbcopy', 'w'],
|
||||
stdin=subprocess.PIPE, close_fds=True)
|
||||
p.communicate(input=text.encode('utf-8'))
|
||||
|
||||
def paste_osx():
|
||||
p = subprocess.Popen(['pbpaste', 'r'],
|
||||
stdout=subprocess.PIPE, close_fds=True)
|
||||
stdout, stderr = p.communicate()
|
||||
return stdout.decode('utf-8')
|
||||
|
||||
return copy_osx, paste_osx
|
||||
|
||||
|
||||
def init_gtk_clipboard():
|
||||
import gtk
|
||||
|
||||
def copy_gtk(text):
|
||||
global cb
|
||||
cb = gtk.Clipboard()
|
||||
cb.set_text(text)
|
||||
cb.store()
|
||||
|
||||
def paste_gtk():
|
||||
clipboardContents = gtk.Clipboard().wait_for_text()
|
||||
# for python 2, returns None if the clipboard is blank.
|
||||
if clipboardContents is None:
|
||||
return ''
|
||||
else:
|
||||
return clipboardContents
|
||||
|
||||
return copy_gtk, paste_gtk
|
||||
|
||||
|
||||
def init_qt_clipboard():
|
||||
# $DISPLAY should exist
|
||||
|
||||
# Try to import from qtpy, but if that fails try PyQt5 then PyQt4
|
||||
try:
|
||||
from qtpy.QtWidgets import QApplication
|
||||
except ImportError:
|
||||
try:
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
except ImportError:
|
||||
from PyQt4.QtGui import QApplication
|
||||
|
||||
app = QApplication.instance()
|
||||
if app is None:
|
||||
app = QApplication([])
|
||||
|
||||
def copy_qt(text):
|
||||
cb = app.clipboard()
|
||||
cb.setText(text)
|
||||
|
||||
def paste_qt():
|
||||
cb = app.clipboard()
|
||||
return text_type(cb.text())
|
||||
|
||||
return copy_qt, paste_qt
|
||||
|
||||
|
||||
def init_xclip_clipboard():
|
||||
def copy_xclip(text):
|
||||
p = subprocess.Popen(['xclip', '-selection', 'c'],
|
||||
stdin=subprocess.PIPE, close_fds=True)
|
||||
p.communicate(input=text.encode('utf-8'))
|
||||
|
||||
def paste_xclip():
|
||||
p = subprocess.Popen(['xclip', '-selection', 'c', '-o'],
|
||||
stdout=subprocess.PIPE, close_fds=True)
|
||||
stdout, stderr = p.communicate()
|
||||
return stdout.decode('utf-8')
|
||||
|
||||
return copy_xclip, paste_xclip
|
||||
|
||||
|
||||
def init_xsel_clipboard():
|
||||
def copy_xsel(text):
|
||||
p = subprocess.Popen(['xsel', '-b', '-i'],
|
||||
stdin=subprocess.PIPE, close_fds=True)
|
||||
p.communicate(input=text.encode('utf-8'))
|
||||
|
||||
def paste_xsel():
|
||||
p = subprocess.Popen(['xsel', '-b', '-o'],
|
||||
stdout=subprocess.PIPE, close_fds=True)
|
||||
stdout, stderr = p.communicate()
|
||||
return stdout.decode('utf-8')
|
||||
|
||||
return copy_xsel, paste_xsel
|
||||
|
||||
|
||||
def init_klipper_clipboard():
|
||||
def copy_klipper(text):
|
||||
p = subprocess.Popen(
|
||||
['qdbus', 'org.kde.klipper', '/klipper', 'setClipboardContents',
|
||||
text.encode('utf-8')],
|
||||
stdin=subprocess.PIPE, close_fds=True)
|
||||
p.communicate(input=None)
|
||||
|
||||
def paste_klipper():
|
||||
p = subprocess.Popen(
|
||||
['qdbus', 'org.kde.klipper', '/klipper', 'getClipboardContents'],
|
||||
stdout=subprocess.PIPE, close_fds=True)
|
||||
stdout, stderr = p.communicate()
|
||||
|
||||
# Workaround for https://bugs.kde.org/show_bug.cgi?id=342874
|
||||
# TODO: https://github.com/asweigart/pyperclip/issues/43
|
||||
clipboardContents = stdout.decode('utf-8')
|
||||
# even if blank, Klipper will append a newline at the end
|
||||
assert len(clipboardContents) > 0
|
||||
# make sure that newline is there
|
||||
assert clipboardContents.endswith('\n')
|
||||
if clipboardContents.endswith('\n'):
|
||||
clipboardContents = clipboardContents[:-1]
|
||||
return clipboardContents
|
||||
|
||||
return copy_klipper, paste_klipper
|
||||
|
||||
|
||||
def init_no_clipboard():
|
||||
class ClipboardUnavailable(object):
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
raise PyperclipException(EXCEPT_MSG)
|
||||
|
||||
if PY2:
|
||||
def __nonzero__(self):
|
||||
return False
|
||||
else:
|
||||
def __bool__(self):
|
||||
return False
|
||||
|
||||
return ClipboardUnavailable(), ClipboardUnavailable()
|
||||
@@ -0,0 +1,12 @@
|
||||
import ctypes
|
||||
|
||||
|
||||
class PyperclipException(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class PyperclipWindowsException(PyperclipException):
|
||||
|
||||
def __init__(self, message):
|
||||
message += " ({err})".format(err=ctypes.WinError())
|
||||
super(PyperclipWindowsException, self).__init__(message)
|
||||
@@ -0,0 +1,153 @@
|
||||
"""
|
||||
This module implements clipboard handling on Windows using ctypes.
|
||||
"""
|
||||
import time
|
||||
import contextlib
|
||||
import ctypes
|
||||
from ctypes import c_size_t, sizeof, c_wchar_p, get_errno, c_wchar
|
||||
from .exceptions import PyperclipWindowsException
|
||||
|
||||
|
||||
class CheckedCall(object):
|
||||
|
||||
def __init__(self, f):
|
||||
super(CheckedCall, self).__setattr__("f", f)
|
||||
|
||||
def __call__(self, *args):
|
||||
ret = self.f(*args)
|
||||
if not ret and get_errno():
|
||||
raise PyperclipWindowsException("Error calling " + self.f.__name__)
|
||||
return ret
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
setattr(self.f, key, value)
|
||||
|
||||
|
||||
def init_windows_clipboard():
|
||||
from ctypes.wintypes import (HGLOBAL, LPVOID, DWORD, LPCSTR, INT, HWND,
|
||||
HINSTANCE, HMENU, BOOL, UINT, HANDLE)
|
||||
|
||||
windll = ctypes.windll
|
||||
|
||||
safeCreateWindowExA = CheckedCall(windll.user32.CreateWindowExA)
|
||||
safeCreateWindowExA.argtypes = [DWORD, LPCSTR, LPCSTR, DWORD, INT, INT,
|
||||
INT, INT, HWND, HMENU, HINSTANCE, LPVOID]
|
||||
safeCreateWindowExA.restype = HWND
|
||||
|
||||
safeDestroyWindow = CheckedCall(windll.user32.DestroyWindow)
|
||||
safeDestroyWindow.argtypes = [HWND]
|
||||
safeDestroyWindow.restype = BOOL
|
||||
|
||||
OpenClipboard = windll.user32.OpenClipboard
|
||||
OpenClipboard.argtypes = [HWND]
|
||||
OpenClipboard.restype = BOOL
|
||||
|
||||
safeCloseClipboard = CheckedCall(windll.user32.CloseClipboard)
|
||||
safeCloseClipboard.argtypes = []
|
||||
safeCloseClipboard.restype = BOOL
|
||||
|
||||
safeEmptyClipboard = CheckedCall(windll.user32.EmptyClipboard)
|
||||
safeEmptyClipboard.argtypes = []
|
||||
safeEmptyClipboard.restype = BOOL
|
||||
|
||||
safeGetClipboardData = CheckedCall(windll.user32.GetClipboardData)
|
||||
safeGetClipboardData.argtypes = [UINT]
|
||||
safeGetClipboardData.restype = HANDLE
|
||||
|
||||
safeSetClipboardData = CheckedCall(windll.user32.SetClipboardData)
|
||||
safeSetClipboardData.argtypes = [UINT, HANDLE]
|
||||
safeSetClipboardData.restype = HANDLE
|
||||
|
||||
safeGlobalAlloc = CheckedCall(windll.kernel32.GlobalAlloc)
|
||||
safeGlobalAlloc.argtypes = [UINT, c_size_t]
|
||||
safeGlobalAlloc.restype = HGLOBAL
|
||||
|
||||
safeGlobalLock = CheckedCall(windll.kernel32.GlobalLock)
|
||||
safeGlobalLock.argtypes = [HGLOBAL]
|
||||
safeGlobalLock.restype = LPVOID
|
||||
|
||||
safeGlobalUnlock = CheckedCall(windll.kernel32.GlobalUnlock)
|
||||
safeGlobalUnlock.argtypes = [HGLOBAL]
|
||||
safeGlobalUnlock.restype = BOOL
|
||||
|
||||
GMEM_MOVEABLE = 0x0002
|
||||
CF_UNICODETEXT = 13
|
||||
|
||||
@contextlib.contextmanager
|
||||
def window():
|
||||
"""
|
||||
Context that provides a valid Windows hwnd.
|
||||
"""
|
||||
# we really just need the hwnd, so setting "STATIC"
|
||||
# as predefined lpClass is just fine.
|
||||
hwnd = safeCreateWindowExA(0, b"STATIC", None, 0, 0, 0, 0, 0,
|
||||
None, None, None, None)
|
||||
try:
|
||||
yield hwnd
|
||||
finally:
|
||||
safeDestroyWindow(hwnd)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def clipboard(hwnd):
|
||||
"""
|
||||
Context manager that opens the clipboard and prevents
|
||||
other applications from modifying the clipboard content.
|
||||
"""
|
||||
# We may not get the clipboard handle immediately because
|
||||
# some other application is accessing it (?)
|
||||
# We try for at least 500ms to get the clipboard.
|
||||
t = time.time() + 0.5
|
||||
success = False
|
||||
while time.time() < t:
|
||||
success = OpenClipboard(hwnd)
|
||||
if success:
|
||||
break
|
||||
time.sleep(0.01)
|
||||
if not success:
|
||||
raise PyperclipWindowsException("Error calling OpenClipboard")
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
safeCloseClipboard()
|
||||
|
||||
def copy_windows(text):
|
||||
# This function is heavily based on
|
||||
# http://msdn.com/ms649016#_win32_Copying_Information_to_the_Clipboard
|
||||
with window() as hwnd:
|
||||
# http://msdn.com/ms649048
|
||||
# If an application calls OpenClipboard with hwnd set to NULL,
|
||||
# EmptyClipboard sets the clipboard owner to NULL;
|
||||
# this causes SetClipboardData to fail.
|
||||
# => We need a valid hwnd to copy something.
|
||||
with clipboard(hwnd):
|
||||
safeEmptyClipboard()
|
||||
|
||||
if text:
|
||||
# http://msdn.com/ms649051
|
||||
# If the hMem parameter identifies a memory object,
|
||||
# the object must have been allocated using the
|
||||
# function with the GMEM_MOVEABLE flag.
|
||||
count = len(text) + 1
|
||||
handle = safeGlobalAlloc(GMEM_MOVEABLE,
|
||||
count * sizeof(c_wchar))
|
||||
locked_handle = safeGlobalLock(handle)
|
||||
|
||||
ctypes.memmove(c_wchar_p(locked_handle),
|
||||
c_wchar_p(text), count * sizeof(c_wchar))
|
||||
|
||||
safeGlobalUnlock(handle)
|
||||
safeSetClipboardData(CF_UNICODETEXT, handle)
|
||||
|
||||
def paste_windows():
|
||||
with clipboard(None):
|
||||
handle = safeGetClipboardData(CF_UNICODETEXT)
|
||||
if not handle:
|
||||
# GetClipboardData may return NULL with errno == NO_ERROR
|
||||
# if the clipboard is empty.
|
||||
# (Also, it may return a handle to an empty buffer,
|
||||
# but technically that's not empty)
|
||||
return ""
|
||||
return c_wchar_p(handle).value
|
||||
|
||||
return copy_windows, paste_windows
|
||||
@@ -0,0 +1,140 @@
|
||||
""" io on the clipboard """
|
||||
from pandas import compat, get_option, option_context, DataFrame
|
||||
from pandas.compat import StringIO, PY2, PY3
|
||||
import warnings
|
||||
|
||||
|
||||
def read_clipboard(sep=r'\s+', **kwargs): # pragma: no cover
|
||||
r"""
|
||||
Read text from clipboard and pass to read_table. See read_table for the
|
||||
full argument list
|
||||
|
||||
Parameters
|
||||
----------
|
||||
sep : str, default '\s+'.
|
||||
A string or regex delimiter. The default of '\s+' denotes
|
||||
one or more whitespace characters.
|
||||
|
||||
Returns
|
||||
-------
|
||||
parsed : DataFrame
|
||||
"""
|
||||
encoding = kwargs.pop('encoding', 'utf-8')
|
||||
|
||||
# only utf-8 is valid for passed value because that's what clipboard
|
||||
# supports
|
||||
if encoding is not None and encoding.lower().replace('-', '') != 'utf8':
|
||||
raise NotImplementedError(
|
||||
'reading from clipboard only supports utf-8 encoding')
|
||||
|
||||
from pandas.io.clipboard import clipboard_get
|
||||
from pandas.io.parsers import read_table
|
||||
text = clipboard_get()
|
||||
|
||||
# try to decode (if needed on PY3)
|
||||
# Strange. linux py33 doesn't complain, win py33 does
|
||||
if PY3:
|
||||
try:
|
||||
text = compat.bytes_to_str(
|
||||
text, encoding=(kwargs.get('encoding') or
|
||||
get_option('display.encoding'))
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Excel copies into clipboard with \t separation
|
||||
# inspect no more then the 10 first lines, if they
|
||||
# all contain an equal number (>0) of tabs, infer
|
||||
# that this came from excel and set 'sep' accordingly
|
||||
lines = text[:10000].split('\n')[:-1][:10]
|
||||
|
||||
# Need to remove leading white space, since read_table
|
||||
# accepts:
|
||||
# a b
|
||||
# 0 1 2
|
||||
# 1 3 4
|
||||
|
||||
counts = {x.lstrip().count('\t') for x in lines}
|
||||
if len(lines) > 1 and len(counts) == 1 and counts.pop() != 0:
|
||||
sep = '\t'
|
||||
|
||||
# Edge case where sep is specified to be None, return to default
|
||||
if sep is None and kwargs.get('delim_whitespace') is None:
|
||||
sep = r'\s+'
|
||||
|
||||
# Regex separator currently only works with python engine.
|
||||
# Default to python if separator is multi-character (regex)
|
||||
if len(sep) > 1 and kwargs.get('engine') is None:
|
||||
kwargs['engine'] = 'python'
|
||||
elif len(sep) > 1 and kwargs.get('engine') == 'c':
|
||||
warnings.warn('read_clipboard with regex separator does not work'
|
||||
' properly with c engine')
|
||||
|
||||
# In PY2, the c table reader first encodes text with UTF-8 but Python
|
||||
# table reader uses the format of the passed string. For consistency,
|
||||
# encode strings for python engine so that output from python and c
|
||||
# engines produce consistent results
|
||||
if kwargs.get('engine') == 'python' and PY2:
|
||||
text = text.encode('utf-8')
|
||||
|
||||
return read_table(StringIO(text), sep=sep, **kwargs)
|
||||
|
||||
|
||||
def to_clipboard(obj, excel=True, sep=None, **kwargs): # pragma: no cover
|
||||
"""
|
||||
Attempt to write text representation of object to the system clipboard
|
||||
The clipboard can be then pasted into Excel for example.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : the object to write to the clipboard
|
||||
excel : boolean, defaults to True
|
||||
if True, use the provided separator, writing in a csv
|
||||
format for allowing easy pasting into excel.
|
||||
if False, write a string representation of the object
|
||||
to the clipboard
|
||||
sep : optional, defaults to tab
|
||||
other keywords are passed to to_csv
|
||||
|
||||
Notes
|
||||
-----
|
||||
Requirements for your platform
|
||||
- Linux: xclip, or xsel (with gtk or PyQt4 modules)
|
||||
- Windows:
|
||||
- OS X:
|
||||
"""
|
||||
encoding = kwargs.pop('encoding', 'utf-8')
|
||||
|
||||
# testing if an invalid encoding is passed to clipboard
|
||||
if encoding is not None and encoding.lower().replace('-', '') != 'utf8':
|
||||
raise ValueError('clipboard only supports utf-8 encoding')
|
||||
|
||||
from pandas.io.clipboard import clipboard_set
|
||||
if excel is None:
|
||||
excel = True
|
||||
|
||||
if excel:
|
||||
try:
|
||||
if sep is None:
|
||||
sep = '\t'
|
||||
buf = StringIO()
|
||||
# clipboard_set (pyperclip) expects unicode
|
||||
obj.to_csv(buf, sep=sep, encoding='utf-8', **kwargs)
|
||||
text = buf.getvalue()
|
||||
if PY2:
|
||||
text = text.decode('utf-8')
|
||||
clipboard_set(text)
|
||||
return
|
||||
except TypeError:
|
||||
warnings.warn('to_clipboard in excel mode requires a single '
|
||||
'character separator.')
|
||||
elif sep is not None:
|
||||
warnings.warn('to_clipboard with excel=False ignores the sep argument')
|
||||
|
||||
if isinstance(obj, DataFrame):
|
||||
# str(df) has various unhelpful defaults, like truncation
|
||||
with option_context('display.max_colwidth', 999999):
|
||||
objstr = obj.to_string(**kwargs)
|
||||
else:
|
||||
objstr = str(obj)
|
||||
clipboard_set(objstr)
|
||||
@@ -0,0 +1,593 @@
|
||||
"""Common IO api utilities"""
|
||||
|
||||
import os
|
||||
import csv
|
||||
import codecs
|
||||
import mmap
|
||||
from contextlib import contextmanager, closing
|
||||
import zipfile
|
||||
|
||||
from pandas.compat import StringIO, BytesIO, string_types, text_type
|
||||
from pandas import compat
|
||||
from pandas.io.formats.printing import pprint_thing
|
||||
import pandas.core.common as com
|
||||
from pandas.core.dtypes.common import is_number, is_file_like
|
||||
|
||||
# compat
|
||||
from pandas.errors import (ParserError, DtypeWarning, # noqa
|
||||
EmptyDataError, ParserWarning)
|
||||
|
||||
# gh-12665: Alias for now and remove later.
|
||||
CParserError = ParserError
|
||||
|
||||
# common NA values
|
||||
# no longer excluding inf representations
|
||||
# '1.#INF','-1.#INF', '1.#INF000000',
|
||||
_NA_VALUES = set([
|
||||
'-1.#IND', '1.#QNAN', '1.#IND', '-1.#QNAN', '#N/A N/A', '#N/A',
|
||||
'N/A', 'n/a', 'NA', '#NA', 'NULL', 'null', 'NaN', '-NaN', 'nan', '-nan', ''
|
||||
])
|
||||
|
||||
|
||||
if compat.PY3:
|
||||
from urllib.request import urlopen, pathname2url
|
||||
_urlopen = urlopen
|
||||
from urllib.parse import urlparse as parse_url
|
||||
from urllib.parse import (uses_relative, uses_netloc, uses_params,
|
||||
urlencode, urljoin)
|
||||
from urllib.error import URLError
|
||||
from http.client import HTTPException # noqa
|
||||
else:
|
||||
from urllib2 import urlopen as _urlopen
|
||||
from urllib import urlencode, pathname2url # noqa
|
||||
from urlparse import urlparse as parse_url
|
||||
from urlparse import uses_relative, uses_netloc, uses_params, urljoin
|
||||
from urllib2 import URLError # noqa
|
||||
from httplib import HTTPException # noqa
|
||||
from contextlib import contextmanager, closing # noqa
|
||||
from functools import wraps # noqa
|
||||
|
||||
# @wraps(_urlopen)
|
||||
@contextmanager
|
||||
def urlopen(*args, **kwargs):
|
||||
with closing(_urlopen(*args, **kwargs)) as f:
|
||||
yield f
|
||||
|
||||
|
||||
_VALID_URLS = set(uses_relative + uses_netloc + uses_params)
|
||||
_VALID_URLS.discard('')
|
||||
|
||||
|
||||
class BaseIterator(object):
|
||||
"""Subclass this and provide a "__next__()" method to obtain an iterator.
|
||||
Useful only when the object being iterated is non-reusable (e.g. OK for a
|
||||
parser, not for an in-memory table, yes for its iterator)."""
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
raise com.AbstractMethodError(self)
|
||||
|
||||
|
||||
if not compat.PY3:
|
||||
BaseIterator.next = lambda self: self.__next__()
|
||||
|
||||
|
||||
def _is_url(url):
|
||||
"""Check to see if a URL has a valid protocol.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
url : str or unicode
|
||||
|
||||
Returns
|
||||
-------
|
||||
isurl : bool
|
||||
If `url` has a valid protocol return True otherwise False.
|
||||
"""
|
||||
try:
|
||||
return parse_url(url).scheme in _VALID_URLS
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def _expand_user(filepath_or_buffer):
|
||||
"""Return the argument with an initial component of ~ or ~user
|
||||
replaced by that user's home directory.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filepath_or_buffer : object to be converted if possible
|
||||
|
||||
Returns
|
||||
-------
|
||||
expanded_filepath_or_buffer : an expanded filepath or the
|
||||
input if not expandable
|
||||
"""
|
||||
if isinstance(filepath_or_buffer, string_types):
|
||||
return os.path.expanduser(filepath_or_buffer)
|
||||
return filepath_or_buffer
|
||||
|
||||
|
||||
def _validate_header_arg(header):
|
||||
if isinstance(header, bool):
|
||||
raise TypeError("Passing a bool to header is invalid. "
|
||||
"Use header=None for no header or "
|
||||
"header=int or list-like of ints to specify "
|
||||
"the row(s) making up the column names")
|
||||
|
||||
|
||||
def _stringify_path(filepath_or_buffer):
|
||||
"""Attempt to convert a path-like object to a string.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filepath_or_buffer : object to be converted
|
||||
|
||||
Returns
|
||||
-------
|
||||
str_filepath_or_buffer : maybe a string version of the object
|
||||
|
||||
Notes
|
||||
-----
|
||||
Objects supporting the fspath protocol (python 3.6+) are coerced
|
||||
according to its __fspath__ method.
|
||||
|
||||
For backwards compatibility with older pythons, pathlib.Path and
|
||||
py.path objects are specially coerced.
|
||||
|
||||
Any other object is passed through unchanged, which includes bytes,
|
||||
strings, buffers, or anything else that's not even path-like.
|
||||
"""
|
||||
try:
|
||||
import pathlib
|
||||
_PATHLIB_INSTALLED = True
|
||||
except ImportError:
|
||||
_PATHLIB_INSTALLED = False
|
||||
|
||||
try:
|
||||
from py.path import local as LocalPath
|
||||
_PY_PATH_INSTALLED = True
|
||||
except ImportError:
|
||||
_PY_PATH_INSTALLED = False
|
||||
|
||||
if hasattr(filepath_or_buffer, '__fspath__'):
|
||||
return filepath_or_buffer.__fspath__()
|
||||
if _PATHLIB_INSTALLED and isinstance(filepath_or_buffer, pathlib.Path):
|
||||
return text_type(filepath_or_buffer)
|
||||
if _PY_PATH_INSTALLED and isinstance(filepath_or_buffer, LocalPath):
|
||||
return filepath_or_buffer.strpath
|
||||
return filepath_or_buffer
|
||||
|
||||
|
||||
def is_s3_url(url):
|
||||
"""Check for an s3, s3n, or s3a url"""
|
||||
try:
|
||||
return parse_url(url).scheme in ['s3', 's3n', 's3a']
|
||||
except: # noqa
|
||||
return False
|
||||
|
||||
|
||||
def get_filepath_or_buffer(filepath_or_buffer, encoding=None,
|
||||
compression=None, mode=None):
|
||||
"""
|
||||
If the filepath_or_buffer is a url, translate and return the buffer.
|
||||
Otherwise passthrough.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filepath_or_buffer : a url, filepath (str, py.path.local or pathlib.Path),
|
||||
or buffer
|
||||
encoding : the encoding to use to decode py3 bytes, default is 'utf-8'
|
||||
mode : str, optional
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple of ({a filepath_ or buffer or S3File instance},
|
||||
encoding, str,
|
||||
compression, str,
|
||||
should_close, bool)
|
||||
"""
|
||||
filepath_or_buffer = _stringify_path(filepath_or_buffer)
|
||||
|
||||
if _is_url(filepath_or_buffer):
|
||||
req = _urlopen(filepath_or_buffer)
|
||||
content_encoding = req.headers.get('Content-Encoding', None)
|
||||
if content_encoding == 'gzip':
|
||||
# Override compression based on Content-Encoding header
|
||||
compression = 'gzip'
|
||||
reader = BytesIO(req.read())
|
||||
req.close()
|
||||
return reader, encoding, compression, True
|
||||
|
||||
if is_s3_url(filepath_or_buffer):
|
||||
from pandas.io import s3
|
||||
return s3.get_filepath_or_buffer(filepath_or_buffer,
|
||||
encoding=encoding,
|
||||
compression=compression,
|
||||
mode=mode)
|
||||
|
||||
if isinstance(filepath_or_buffer, (compat.string_types,
|
||||
compat.binary_type,
|
||||
mmap.mmap)):
|
||||
return _expand_user(filepath_or_buffer), None, compression, False
|
||||
|
||||
if not is_file_like(filepath_or_buffer):
|
||||
msg = "Invalid file path or buffer object type: {_type}"
|
||||
raise ValueError(msg.format(_type=type(filepath_or_buffer)))
|
||||
|
||||
return filepath_or_buffer, None, compression, False
|
||||
|
||||
|
||||
def file_path_to_url(path):
|
||||
"""
|
||||
converts an absolute native path to a FILE URL.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : a path in native format
|
||||
|
||||
Returns
|
||||
-------
|
||||
a valid FILE URL
|
||||
"""
|
||||
return urljoin('file:', pathname2url(path))
|
||||
|
||||
|
||||
_compression_to_extension = {
|
||||
'gzip': '.gz',
|
||||
'bz2': '.bz2',
|
||||
'zip': '.zip',
|
||||
'xz': '.xz',
|
||||
}
|
||||
|
||||
|
||||
def _infer_compression(filepath_or_buffer, compression):
|
||||
"""
|
||||
Get the compression method for filepath_or_buffer. If compression='infer',
|
||||
the inferred compression method is returned. Otherwise, the input
|
||||
compression method is returned unchanged, unless it's invalid, in which
|
||||
case an error is raised.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filepath_or_buf :
|
||||
a path (str) or buffer
|
||||
compression : str or None
|
||||
the compression method including None for no compression and 'infer'
|
||||
|
||||
Returns
|
||||
-------
|
||||
string or None :
|
||||
compression method
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError on invalid compression specified
|
||||
"""
|
||||
|
||||
# No compression has been explicitly specified
|
||||
if compression is None:
|
||||
return None
|
||||
|
||||
# Infer compression
|
||||
if compression == 'infer':
|
||||
# Convert all path types (e.g. pathlib.Path) to strings
|
||||
filepath_or_buffer = _stringify_path(filepath_or_buffer)
|
||||
if not isinstance(filepath_or_buffer, compat.string_types):
|
||||
# Cannot infer compression of a buffer, assume no compression
|
||||
return None
|
||||
|
||||
# Infer compression from the filename/URL extension
|
||||
for compression, extension in _compression_to_extension.items():
|
||||
if filepath_or_buffer.endswith(extension):
|
||||
return compression
|
||||
return None
|
||||
|
||||
# Compression has been specified. Check that it's valid
|
||||
if compression in _compression_to_extension:
|
||||
return compression
|
||||
|
||||
msg = 'Unrecognized compression type: {}'.format(compression)
|
||||
valid = ['infer', None] + sorted(_compression_to_extension)
|
||||
msg += '\nValid compression types are {}'.format(valid)
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def _get_handle(path_or_buf, mode, encoding=None, compression=None,
|
||||
memory_map=False, is_text=True):
|
||||
"""
|
||||
Get file handle for given path/buffer and mode.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path_or_buf :
|
||||
a path (str) or buffer
|
||||
mode : str
|
||||
mode to open path_or_buf with
|
||||
encoding : str or None
|
||||
compression : str or None
|
||||
Supported compression protocols are gzip, bz2, zip, and xz
|
||||
memory_map : boolean, default False
|
||||
See parsers._parser_params for more information.
|
||||
is_text : boolean, default True
|
||||
whether file/buffer is in text format (csv, json, etc.), or in binary
|
||||
mode (pickle, etc.)
|
||||
|
||||
Returns
|
||||
-------
|
||||
f : file-like
|
||||
A file-like object
|
||||
handles : list of file-like objects
|
||||
A list of file-like object that were opened in this function.
|
||||
"""
|
||||
try:
|
||||
from s3fs import S3File
|
||||
need_text_wrapping = (BytesIO, S3File)
|
||||
except ImportError:
|
||||
need_text_wrapping = (BytesIO,)
|
||||
|
||||
handles = list()
|
||||
f = path_or_buf
|
||||
|
||||
# Convert pathlib.Path/py.path.local or string
|
||||
path_or_buf = _stringify_path(path_or_buf)
|
||||
is_path = isinstance(path_or_buf, compat.string_types)
|
||||
|
||||
if compression:
|
||||
|
||||
if compat.PY2 and not is_path and encoding:
|
||||
msg = 'compression with encoding is not yet supported in Python 2'
|
||||
raise ValueError(msg)
|
||||
|
||||
# GZ Compression
|
||||
if compression == 'gzip':
|
||||
import gzip
|
||||
if is_path:
|
||||
f = gzip.open(path_or_buf, mode)
|
||||
else:
|
||||
f = gzip.GzipFile(fileobj=path_or_buf)
|
||||
|
||||
# BZ Compression
|
||||
elif compression == 'bz2':
|
||||
import bz2
|
||||
if is_path:
|
||||
f = bz2.BZ2File(path_or_buf, mode)
|
||||
elif compat.PY2:
|
||||
# Python 2's bz2 module can't take file objects, so have to
|
||||
# run through decompress manually
|
||||
f = StringIO(bz2.decompress(path_or_buf.read()))
|
||||
path_or_buf.close()
|
||||
else:
|
||||
f = bz2.BZ2File(path_or_buf)
|
||||
|
||||
# ZIP Compression
|
||||
elif compression == 'zip':
|
||||
zf = BytesZipFile(path_or_buf, mode)
|
||||
if zf.mode == 'w':
|
||||
f = zf
|
||||
elif zf.mode == 'r':
|
||||
zip_names = zf.namelist()
|
||||
if len(zip_names) == 1:
|
||||
f = zf.open(zip_names.pop())
|
||||
elif len(zip_names) == 0:
|
||||
raise ValueError('Zero files found in ZIP file {}'
|
||||
.format(path_or_buf))
|
||||
else:
|
||||
raise ValueError('Multiple files found in ZIP file.'
|
||||
' Only one file per ZIP: {}'
|
||||
.format(zip_names))
|
||||
|
||||
# XZ Compression
|
||||
elif compression == 'xz':
|
||||
lzma = compat.import_lzma()
|
||||
f = lzma.LZMAFile(path_or_buf, mode)
|
||||
|
||||
# Unrecognized Compression
|
||||
else:
|
||||
msg = 'Unrecognized compression type: {}'.format(compression)
|
||||
raise ValueError(msg)
|
||||
|
||||
handles.append(f)
|
||||
|
||||
elif is_path:
|
||||
if compat.PY2:
|
||||
# Python 2
|
||||
f = open(path_or_buf, mode)
|
||||
elif encoding:
|
||||
# Python 3 and encoding
|
||||
f = open(path_or_buf, mode, encoding=encoding)
|
||||
elif is_text:
|
||||
# Python 3 and no explicit encoding
|
||||
f = open(path_or_buf, mode, errors='replace')
|
||||
else:
|
||||
# Python 3 and binary mode
|
||||
f = open(path_or_buf, mode)
|
||||
handles.append(f)
|
||||
|
||||
# in Python 3, convert BytesIO or fileobjects passed with an encoding
|
||||
if compat.PY3 and is_text and\
|
||||
(compression or isinstance(f, need_text_wrapping)):
|
||||
from io import TextIOWrapper
|
||||
f = TextIOWrapper(f, encoding=encoding)
|
||||
handles.append(f)
|
||||
|
||||
if memory_map and hasattr(f, 'fileno'):
|
||||
try:
|
||||
g = MMapWrapper(f)
|
||||
f.close()
|
||||
f = g
|
||||
except Exception:
|
||||
# we catch any errors that may have occurred
|
||||
# because that is consistent with the lower-level
|
||||
# functionality of the C engine (pd.read_csv), so
|
||||
# leave the file handler as is then
|
||||
pass
|
||||
|
||||
return f, handles
|
||||
|
||||
|
||||
class BytesZipFile(zipfile.ZipFile, BytesIO):
|
||||
"""
|
||||
Wrapper for standard library class ZipFile and allow the returned file-like
|
||||
handle to accept byte strings via `write` method.
|
||||
|
||||
BytesIO provides attributes of file-like object and ZipFile.writestr writes
|
||||
bytes strings into a member of the archive.
|
||||
"""
|
||||
# GH 17778
|
||||
def __init__(self, file, mode, compression=zipfile.ZIP_DEFLATED, **kwargs):
|
||||
if mode in ['wb', 'rb']:
|
||||
mode = mode.replace('b', '')
|
||||
super(BytesZipFile, self).__init__(file, mode, compression, **kwargs)
|
||||
|
||||
def write(self, data):
|
||||
super(BytesZipFile, self).writestr(self.filename, data)
|
||||
|
||||
@property
|
||||
def closed(self):
|
||||
return self.fp is None
|
||||
|
||||
|
||||
class MMapWrapper(BaseIterator):
|
||||
"""
|
||||
Wrapper for the Python's mmap class so that it can be properly read in
|
||||
by Python's csv.reader class.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
f : file object
|
||||
File object to be mapped onto memory. Must support the 'fileno'
|
||||
method or have an equivalent attribute
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, f):
|
||||
self.mmap = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.mmap, name)
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
newline = self.mmap.readline()
|
||||
|
||||
# readline returns bytes, not str, in Python 3,
|
||||
# but Python's CSV reader expects str, so convert
|
||||
# the output to str before continuing
|
||||
if compat.PY3:
|
||||
newline = compat.bytes_to_str(newline)
|
||||
|
||||
# mmap doesn't raise if reading past the allocated
|
||||
# data but instead returns an empty string, so raise
|
||||
# if that is returned
|
||||
if newline == '':
|
||||
raise StopIteration
|
||||
return newline
|
||||
|
||||
|
||||
if not compat.PY3:
|
||||
MMapWrapper.next = lambda self: self.__next__()
|
||||
|
||||
|
||||
class UTF8Recoder(BaseIterator):
|
||||
|
||||
"""
|
||||
Iterator that reads an encoded stream and reencodes the input to UTF-8
|
||||
"""
|
||||
|
||||
def __init__(self, f, encoding):
|
||||
self.reader = codecs.getreader(encoding)(f)
|
||||
|
||||
def read(self, bytes=-1):
|
||||
return self.reader.read(bytes).encode("utf-8")
|
||||
|
||||
def readline(self):
|
||||
return self.reader.readline().encode("utf-8")
|
||||
|
||||
def next(self):
|
||||
return next(self.reader).encode("utf-8")
|
||||
|
||||
|
||||
if compat.PY3: # pragma: no cover
|
||||
def UnicodeReader(f, dialect=csv.excel, encoding="utf-8", **kwds):
|
||||
# ignore encoding
|
||||
return csv.reader(f, dialect=dialect, **kwds)
|
||||
|
||||
def UnicodeWriter(f, dialect=csv.excel, encoding="utf-8", **kwds):
|
||||
return csv.writer(f, dialect=dialect, **kwds)
|
||||
else:
|
||||
class UnicodeReader(BaseIterator):
|
||||
|
||||
"""
|
||||
A CSV reader which will iterate over lines in the CSV file "f",
|
||||
which is encoded in the given encoding.
|
||||
|
||||
On Python 3, this is replaced (below) by csv.reader, which handles
|
||||
unicode.
|
||||
"""
|
||||
|
||||
def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
|
||||
f = UTF8Recoder(f, encoding)
|
||||
self.reader = csv.reader(f, dialect=dialect, **kwds)
|
||||
|
||||
def __next__(self):
|
||||
row = next(self.reader)
|
||||
return [compat.text_type(s, "utf-8") for s in row]
|
||||
|
||||
class UnicodeWriter(object):
|
||||
|
||||
"""
|
||||
A CSV writer which will write rows to CSV file "f",
|
||||
which is encoded in the given encoding.
|
||||
"""
|
||||
|
||||
def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
|
||||
# Redirect output to a queue
|
||||
self.queue = StringIO()
|
||||
self.writer = csv.writer(self.queue, dialect=dialect, **kwds)
|
||||
self.stream = f
|
||||
self.encoder = codecs.getincrementalencoder(encoding)()
|
||||
self.quoting = kwds.get("quoting", None)
|
||||
|
||||
def writerow(self, row):
|
||||
def _check_as_is(x):
|
||||
return (self.quoting == csv.QUOTE_NONNUMERIC and
|
||||
is_number(x)) or isinstance(x, str)
|
||||
|
||||
row = [x if _check_as_is(x)
|
||||
else pprint_thing(x).encode("utf-8") for x in row]
|
||||
|
||||
self.writer.writerow([s for s in row])
|
||||
# Fetch UTF-8 output from the queue ...
|
||||
data = self.queue.getvalue()
|
||||
data = data.decode("utf-8")
|
||||
# ... and re-encode it into the target encoding
|
||||
data = self.encoder.encode(data)
|
||||
# write to the target stream
|
||||
self.stream.write(data)
|
||||
# empty queue
|
||||
self.queue.truncate(0)
|
||||
|
||||
def writerows(self, rows):
|
||||
def _check_as_is(x):
|
||||
return (self.quoting == csv.QUOTE_NONNUMERIC and
|
||||
is_number(x)) or isinstance(x, str)
|
||||
|
||||
for i, row in enumerate(rows):
|
||||
rows[i] = [x if _check_as_is(x)
|
||||
else pprint_thing(x).encode("utf-8") for x in row]
|
||||
|
||||
self.writer.writerows([[s for s in row] for row in rows])
|
||||
# Fetch UTF-8 output from the queue ...
|
||||
data = self.queue.getvalue()
|
||||
data = data.decode("utf-8")
|
||||
# ... and re-encode it into the target encoding
|
||||
data = self.encoder.encode(data)
|
||||
# write to the target stream
|
||||
self.stream.write(data)
|
||||
# empty queue
|
||||
self.queue.truncate(0)
|
||||
@@ -0,0 +1,63 @@
|
||||
"""This module is designed for community supported date conversion functions"""
|
||||
from pandas.compat import range, map
|
||||
import numpy as np
|
||||
from pandas._libs.tslibs import parsing
|
||||
|
||||
|
||||
def parse_date_time(date_col, time_col):
|
||||
date_col = _maybe_cast(date_col)
|
||||
time_col = _maybe_cast(time_col)
|
||||
return parsing.try_parse_date_and_time(date_col, time_col)
|
||||
|
||||
|
||||
def parse_date_fields(year_col, month_col, day_col):
|
||||
year_col = _maybe_cast(year_col)
|
||||
month_col = _maybe_cast(month_col)
|
||||
day_col = _maybe_cast(day_col)
|
||||
return parsing.try_parse_year_month_day(year_col, month_col, day_col)
|
||||
|
||||
|
||||
def parse_all_fields(year_col, month_col, day_col, hour_col, minute_col,
|
||||
second_col):
|
||||
year_col = _maybe_cast(year_col)
|
||||
month_col = _maybe_cast(month_col)
|
||||
day_col = _maybe_cast(day_col)
|
||||
hour_col = _maybe_cast(hour_col)
|
||||
minute_col = _maybe_cast(minute_col)
|
||||
second_col = _maybe_cast(second_col)
|
||||
return parsing.try_parse_datetime_components(year_col, month_col, day_col,
|
||||
hour_col, minute_col,
|
||||
second_col)
|
||||
|
||||
|
||||
def generic_parser(parse_func, *cols):
|
||||
N = _check_columns(cols)
|
||||
results = np.empty(N, dtype=object)
|
||||
|
||||
for i in range(N):
|
||||
args = [c[i] for c in cols]
|
||||
results[i] = parse_func(*args)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def _maybe_cast(arr):
|
||||
if not arr.dtype.type == np.object_:
|
||||
arr = np.array(arr, dtype=object)
|
||||
return arr
|
||||
|
||||
|
||||
def _check_columns(cols):
|
||||
if not len(cols):
|
||||
raise AssertionError("There must be at least 1 column")
|
||||
|
||||
head, tail = cols[0], cols[1:]
|
||||
|
||||
N = len(head)
|
||||
|
||||
for i, n in enumerate(map(len, tail)):
|
||||
if n != N:
|
||||
raise AssertionError('All columns must have the same length: {0}; '
|
||||
'column {1} has length {2}'.format(N, i, n))
|
||||
|
||||
return N
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,112 @@
|
||||
""" feather-format compat """
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
from pandas import DataFrame, RangeIndex, Int64Index
|
||||
from pandas.compat import range
|
||||
from pandas.io.common import _stringify_path
|
||||
|
||||
|
||||
def _try_import():
|
||||
# since pandas is a dependency of feather
|
||||
# we need to import on first use
|
||||
|
||||
try:
|
||||
import feather
|
||||
except ImportError:
|
||||
|
||||
# give a nice error message
|
||||
raise ImportError("the feather-format library is not installed\n"
|
||||
"you can install via conda\n"
|
||||
"conda install feather-format -c conda-forge\n"
|
||||
"or via pip\n"
|
||||
"pip install -U feather-format\n")
|
||||
|
||||
try:
|
||||
LooseVersion(feather.__version__) >= LooseVersion('0.3.1')
|
||||
except AttributeError:
|
||||
raise ImportError("the feather-format library must be >= "
|
||||
"version 0.3.1\n"
|
||||
"you can install via conda\n"
|
||||
"conda install feather-format -c conda-forge"
|
||||
"or via pip\n"
|
||||
"pip install -U feather-format\n")
|
||||
|
||||
return feather
|
||||
|
||||
|
||||
def to_feather(df, path):
|
||||
"""
|
||||
Write a DataFrame to the feather-format
|
||||
|
||||
Parameters
|
||||
----------
|
||||
df : DataFrame
|
||||
path : string file path, or file-like object
|
||||
|
||||
"""
|
||||
path = _stringify_path(path)
|
||||
if not isinstance(df, DataFrame):
|
||||
raise ValueError("feather only support IO with DataFrames")
|
||||
|
||||
feather = _try_import()
|
||||
valid_types = {'string', 'unicode'}
|
||||
|
||||
# validate index
|
||||
# --------------
|
||||
|
||||
# validate that we have only a default index
|
||||
# raise on anything else as we don't serialize the index
|
||||
|
||||
if not isinstance(df.index, Int64Index):
|
||||
raise ValueError("feather does not support serializing {} "
|
||||
"for the index; you can .reset_index()"
|
||||
"to make the index into column(s)".format(
|
||||
type(df.index)))
|
||||
|
||||
if not df.index.equals(RangeIndex.from_range(range(len(df)))):
|
||||
raise ValueError("feather does not support serializing a "
|
||||
"non-default index for the index; you "
|
||||
"can .reset_index() to make the index "
|
||||
"into column(s)")
|
||||
|
||||
if df.index.name is not None:
|
||||
raise ValueError("feather does not serialize index meta-data on a "
|
||||
"default index")
|
||||
|
||||
# validate columns
|
||||
# ----------------
|
||||
|
||||
# must have value column names (strings only)
|
||||
if df.columns.inferred_type not in valid_types:
|
||||
raise ValueError("feather must have string column names")
|
||||
|
||||
feather.write_dataframe(df, path)
|
||||
|
||||
|
||||
def read_feather(path, nthreads=1):
|
||||
"""
|
||||
Load a feather-format object from the file path
|
||||
|
||||
.. versionadded 0.20.0
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : string file path, or file-like object
|
||||
nthreads : int, default 1
|
||||
Number of CPU threads to use when reading to pandas.DataFrame
|
||||
|
||||
.. versionadded 0.21.0
|
||||
|
||||
Returns
|
||||
-------
|
||||
type of object stored in file
|
||||
|
||||
"""
|
||||
|
||||
feather = _try_import()
|
||||
path = _stringify_path(path)
|
||||
|
||||
if LooseVersion(feather.__version__) < LooseVersion('0.4.0'):
|
||||
return feather.read_dataframe(path)
|
||||
|
||||
return feather.read_dataframe(path, nthreads=nthreads)
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
Internal module for console introspection
|
||||
"""
|
||||
|
||||
import sys
|
||||
import locale
|
||||
from pandas.io.formats.terminal import get_terminal_size
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Global formatting options
|
||||
_initial_defencoding = None
|
||||
|
||||
|
||||
def detect_console_encoding():
|
||||
"""
|
||||
Try to find the most capable encoding supported by the console.
|
||||
slightly modified from the way IPython handles the same issue.
|
||||
"""
|
||||
global _initial_defencoding
|
||||
|
||||
encoding = None
|
||||
try:
|
||||
encoding = sys.stdout.encoding or sys.stdin.encoding
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# try again for something better
|
||||
if not encoding or 'ascii' in encoding.lower():
|
||||
try:
|
||||
encoding = locale.getpreferredencoding()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# when all else fails. this will usually be "ascii"
|
||||
if not encoding or 'ascii' in encoding.lower():
|
||||
encoding = sys.getdefaultencoding()
|
||||
|
||||
# GH3360, save the reported defencoding at import time
|
||||
# MPL backends may change it. Make available for debugging.
|
||||
if not _initial_defencoding:
|
||||
_initial_defencoding = sys.getdefaultencoding()
|
||||
|
||||
return encoding
|
||||
|
||||
|
||||
def get_console_size():
|
||||
"""Return console size as tuple = (width, height).
|
||||
|
||||
Returns (None,None) in non-interactive session.
|
||||
"""
|
||||
from pandas import get_option
|
||||
from pandas.core import common as com
|
||||
|
||||
display_width = get_option('display.width')
|
||||
# deprecated.
|
||||
display_height = get_option('display.max_rows')
|
||||
|
||||
# Consider
|
||||
# interactive shell terminal, can detect term size
|
||||
# interactive non-shell terminal (ipnb/ipqtconsole), cannot detect term
|
||||
# size non-interactive script, should disregard term size
|
||||
|
||||
# in addition
|
||||
# width,height have default values, but setting to 'None' signals
|
||||
# should use Auto-Detection, But only in interactive shell-terminal.
|
||||
# Simple. yeah.
|
||||
|
||||
if com.in_interactive_session():
|
||||
if com.in_ipython_frontend():
|
||||
# sane defaults for interactive non-shell terminal
|
||||
# match default for width,height in config_init
|
||||
from pandas.core.config import get_default_val
|
||||
terminal_width = get_default_val('display.width')
|
||||
terminal_height = get_default_val('display.max_rows')
|
||||
else:
|
||||
# pure terminal
|
||||
terminal_width, terminal_height = get_terminal_size()
|
||||
else:
|
||||
terminal_width, terminal_height = None, None
|
||||
|
||||
# Note if the User sets width/Height to None (auto-detection)
|
||||
# and we're in a script (non-inter), this will return (None,None)
|
||||
# caller needs to deal.
|
||||
return (display_width or terminal_width, display_height or terminal_height)
|
||||
@@ -0,0 +1,250 @@
|
||||
"""Utilities for interpreting CSS from Stylers for formatting non-HTML outputs
|
||||
"""
|
||||
|
||||
import re
|
||||
import warnings
|
||||
|
||||
|
||||
class CSSWarning(UserWarning):
|
||||
"""This CSS syntax cannot currently be parsed"""
|
||||
pass
|
||||
|
||||
|
||||
class CSSResolver(object):
|
||||
"""A callable for parsing and resolving CSS to atomic properties
|
||||
|
||||
"""
|
||||
|
||||
INITIAL_STYLE = {
|
||||
}
|
||||
|
||||
def __call__(self, declarations_str, inherited=None):
|
||||
""" the given declarations to atomic properties
|
||||
|
||||
Parameters
|
||||
----------
|
||||
declarations_str : str
|
||||
A list of CSS declarations
|
||||
inherited : dict, optional
|
||||
Atomic properties indicating the inherited style context in which
|
||||
declarations_str is to be resolved. ``inherited`` should already
|
||||
be resolved, i.e. valid output of this method.
|
||||
|
||||
Returns
|
||||
-------
|
||||
props : dict
|
||||
Atomic CSS 2.2 properties
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> resolve = CSSResolver()
|
||||
>>> inherited = {'font-family': 'serif', 'font-weight': 'bold'}
|
||||
>>> out = resolve('''
|
||||
... border-color: BLUE RED;
|
||||
... font-size: 1em;
|
||||
... font-size: 2em;
|
||||
... font-weight: normal;
|
||||
... font-weight: inherit;
|
||||
... ''', inherited)
|
||||
>>> sorted(out.items()) # doctest: +NORMALIZE_WHITESPACE
|
||||
[('border-bottom-color', 'blue'),
|
||||
('border-left-color', 'red'),
|
||||
('border-right-color', 'red'),
|
||||
('border-top-color', 'blue'),
|
||||
('font-family', 'serif'),
|
||||
('font-size', '24pt'),
|
||||
('font-weight', 'bold')]
|
||||
"""
|
||||
|
||||
props = dict(self.atomize(self.parse(declarations_str)))
|
||||
if inherited is None:
|
||||
inherited = {}
|
||||
|
||||
# 1. resolve inherited, initial
|
||||
for prop, val in inherited.items():
|
||||
if prop not in props:
|
||||
props[prop] = val
|
||||
|
||||
for prop, val in list(props.items()):
|
||||
if val == 'inherit':
|
||||
val = inherited.get(prop, 'initial')
|
||||
if val == 'initial':
|
||||
val = self.INITIAL_STYLE.get(prop)
|
||||
|
||||
if val is None:
|
||||
# we do not define a complete initial stylesheet
|
||||
del props[prop]
|
||||
else:
|
||||
props[prop] = val
|
||||
|
||||
# 2. resolve relative font size
|
||||
if props.get('font-size'):
|
||||
if 'font-size' in inherited:
|
||||
em_pt = inherited['font-size']
|
||||
assert em_pt[-2:] == 'pt'
|
||||
em_pt = float(em_pt[:-2])
|
||||
else:
|
||||
em_pt = None
|
||||
props['font-size'] = self.size_to_pt(
|
||||
props['font-size'], em_pt, conversions=self.FONT_SIZE_RATIOS)
|
||||
|
||||
font_size = float(props['font-size'][:-2])
|
||||
else:
|
||||
font_size = None
|
||||
|
||||
# 3. TODO: resolve other font-relative units
|
||||
for side in self.SIDES:
|
||||
prop = 'border-{side}-width'.format(side=side)
|
||||
if prop in props:
|
||||
props[prop] = self.size_to_pt(
|
||||
props[prop], em_pt=font_size,
|
||||
conversions=self.BORDER_WIDTH_RATIOS)
|
||||
for prop in ['margin-{side}'.format(side=side),
|
||||
'padding-{side}'.format(side=side)]:
|
||||
if prop in props:
|
||||
# TODO: support %
|
||||
props[prop] = self.size_to_pt(
|
||||
props[prop], em_pt=font_size,
|
||||
conversions=self.MARGIN_RATIOS)
|
||||
|
||||
return props
|
||||
|
||||
UNIT_RATIOS = {
|
||||
'rem': ('pt', 12),
|
||||
'ex': ('em', .5),
|
||||
# 'ch':
|
||||
'px': ('pt', .75),
|
||||
'pc': ('pt', 12),
|
||||
'in': ('pt', 72),
|
||||
'cm': ('in', 1 / 2.54),
|
||||
'mm': ('in', 1 / 25.4),
|
||||
'q': ('mm', .25),
|
||||
'!!default': ('em', 0),
|
||||
}
|
||||
|
||||
FONT_SIZE_RATIOS = UNIT_RATIOS.copy()
|
||||
FONT_SIZE_RATIOS.update({
|
||||
'%': ('em', .01),
|
||||
'xx-small': ('rem', .5),
|
||||
'x-small': ('rem', .625),
|
||||
'small': ('rem', .8),
|
||||
'medium': ('rem', 1),
|
||||
'large': ('rem', 1.125),
|
||||
'x-large': ('rem', 1.5),
|
||||
'xx-large': ('rem', 2),
|
||||
'smaller': ('em', 1 / 1.2),
|
||||
'larger': ('em', 1.2),
|
||||
'!!default': ('em', 1),
|
||||
})
|
||||
|
||||
MARGIN_RATIOS = UNIT_RATIOS.copy()
|
||||
MARGIN_RATIOS.update({
|
||||
'none': ('pt', 0),
|
||||
})
|
||||
|
||||
BORDER_WIDTH_RATIOS = UNIT_RATIOS.copy()
|
||||
BORDER_WIDTH_RATIOS.update({
|
||||
'none': ('pt', 0),
|
||||
'thick': ('px', 4),
|
||||
'medium': ('px', 2),
|
||||
'thin': ('px', 1),
|
||||
# Default: medium only if solid
|
||||
})
|
||||
|
||||
def size_to_pt(self, in_val, em_pt=None, conversions=UNIT_RATIOS):
|
||||
def _error():
|
||||
warnings.warn('Unhandled size: {val!r}'.format(val=in_val),
|
||||
CSSWarning)
|
||||
return self.size_to_pt('1!!default', conversions=conversions)
|
||||
|
||||
try:
|
||||
val, unit = re.match(r'^(\S*?)([a-zA-Z%!].*)', in_val).groups()
|
||||
except AttributeError:
|
||||
return _error()
|
||||
if val == '':
|
||||
# hack for 'large' etc.
|
||||
val = 1
|
||||
else:
|
||||
try:
|
||||
val = float(val)
|
||||
except ValueError:
|
||||
return _error()
|
||||
|
||||
while unit != 'pt':
|
||||
if unit == 'em':
|
||||
if em_pt is None:
|
||||
unit = 'rem'
|
||||
else:
|
||||
val *= em_pt
|
||||
unit = 'pt'
|
||||
continue
|
||||
|
||||
try:
|
||||
unit, mul = conversions[unit]
|
||||
except KeyError:
|
||||
return _error()
|
||||
val *= mul
|
||||
|
||||
val = round(val, 5)
|
||||
if int(val) == val:
|
||||
size_fmt = '{fmt:d}pt'.format(fmt=int(val))
|
||||
else:
|
||||
size_fmt = '{fmt:f}pt'.format(fmt=val)
|
||||
return size_fmt
|
||||
|
||||
def atomize(self, declarations):
|
||||
for prop, value in declarations:
|
||||
attr = 'expand_' + prop.replace('-', '_')
|
||||
try:
|
||||
expand = getattr(self, attr)
|
||||
except AttributeError:
|
||||
yield prop, value
|
||||
else:
|
||||
for prop, value in expand(prop, value):
|
||||
yield prop, value
|
||||
|
||||
SIDE_SHORTHANDS = {
|
||||
1: [0, 0, 0, 0],
|
||||
2: [0, 1, 0, 1],
|
||||
3: [0, 1, 2, 1],
|
||||
4: [0, 1, 2, 3],
|
||||
}
|
||||
SIDES = ('top', 'right', 'bottom', 'left')
|
||||
|
||||
def _side_expander(prop_fmt):
|
||||
def expand(self, prop, value):
|
||||
tokens = value.split()
|
||||
try:
|
||||
mapping = self.SIDE_SHORTHANDS[len(tokens)]
|
||||
except KeyError:
|
||||
warnings.warn('Could not expand "{prop}: {val}"'
|
||||
.format(prop=prop, val=value), CSSWarning)
|
||||
return
|
||||
for key, idx in zip(self.SIDES, mapping):
|
||||
yield prop_fmt.format(key), tokens[idx]
|
||||
|
||||
return expand
|
||||
|
||||
expand_border_color = _side_expander('border-{:s}-color')
|
||||
expand_border_style = _side_expander('border-{:s}-style')
|
||||
expand_border_width = _side_expander('border-{:s}-width')
|
||||
expand_margin = _side_expander('margin-{:s}')
|
||||
expand_padding = _side_expander('padding-{:s}')
|
||||
|
||||
def parse(self, declarations_str):
|
||||
"""Generates (prop, value) pairs from declarations
|
||||
|
||||
In a future version may generate parsed tokens from tinycss/tinycss2
|
||||
"""
|
||||
for decl in declarations_str.split(';'):
|
||||
if not decl.strip():
|
||||
continue
|
||||
prop, sep, val = decl.partition(':')
|
||||
prop = prop.strip().lower()
|
||||
# TODO: don't lowercase case sensitive parts of values (strings)
|
||||
val = val.strip().lower()
|
||||
if sep:
|
||||
yield prop, val
|
||||
else:
|
||||
warnings.warn('Ill-formatted attribute: expected a colon '
|
||||
'in {decl!r}'.format(decl=decl), CSSWarning)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user