mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-07 11:58:11 +08:00
Fixing bugs in cxg conversion tool (#1782)
This commit is contained in:
+3
-1
@@ -1,9 +1,10 @@
|
|||||||
import click
|
import click
|
||||||
|
|
||||||
from .. import __version__
|
from .convert_to_cxg import convert_to_cxg
|
||||||
from .launch import launch
|
from .launch import launch
|
||||||
from .prepare import prepare
|
from .prepare import prepare
|
||||||
from .upgrade import log_upgrade_check
|
from .upgrade import log_upgrade_check
|
||||||
|
from .. import __version__
|
||||||
|
|
||||||
|
|
||||||
@click.group(
|
@click.group(
|
||||||
@@ -29,3 +30,4 @@ def cli(upgrade_check):
|
|||||||
|
|
||||||
cli.add_command(launch)
|
cli.add_command(launch)
|
||||||
cli.add_command(prepare)
|
cli.add_command(prepare)
|
||||||
|
cli.add_command(convert_to_cxg)
|
||||||
|
|||||||
@@ -16,12 +16,11 @@ from server.converters.h5ad_data_file import H5ADDataFile
|
|||||||
@click.argument(
|
@click.argument(
|
||||||
"input-file",
|
"input-file",
|
||||||
nargs=1,
|
nargs=1,
|
||||||
help="Path to the H5AD input file to be converted.",
|
|
||||||
type=click.Path(exists=True, dir_okay=False),
|
type=click.Path(exists=True, dir_okay=False),
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"-o",
|
"-o",
|
||||||
"--output-dir",
|
"--output-directory",
|
||||||
help="Name of the output CXG directory. If not provided, will default to be the input filename with a "
|
help="Name of the output CXG directory. If not provided, will default to be the input filename with a "
|
||||||
"CXG extension.",
|
"CXG extension.",
|
||||||
)
|
)
|
||||||
@@ -70,9 +69,9 @@ from server.converters.h5ad_data_file import H5ADDataFile
|
|||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--disable-corpora-schema",
|
"--disable-corpora-schema",
|
||||||
"When set, conversion process will neither extract nor store Corpora schema information. See "
|
help="When set, conversion process will neither extract nor store Corpora schema information. See "
|
||||||
"https://github.com/chanzuckerberg/corpora-data-portal/blob/main/backend/schema/corpora_schema.md for "
|
"https://github.com/chanzuckerberg/corpora-data-portal/blob/main/backend/schema/corpora_schema.md for more "
|
||||||
"more information.",
|
"information.",
|
||||||
default=False,
|
default=False,
|
||||||
show_default=True,
|
show_default=True,
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
@@ -84,7 +83,6 @@ from server.converters.h5ad_data_file import H5ADDataFile
|
|||||||
show_default=True,
|
show_default=True,
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
)
|
)
|
||||||
@click.option("-v", "--verbose", count=True)
|
|
||||||
@click.help_option("--help", "-h", help="Show this message and exit.")
|
@click.help_option("--help", "-h", help="Show this message and exit.")
|
||||||
def convert_to_cxg(
|
def convert_to_cxg(
|
||||||
input_file,
|
input_file,
|
||||||
@@ -97,7 +95,7 @@ def convert_to_cxg(
|
|||||||
var_names,
|
var_names,
|
||||||
disable_custom_colors,
|
disable_custom_colors,
|
||||||
disable_corpora_schema,
|
disable_corpora_schema,
|
||||||
should_overwrite,
|
overwrite,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Convert a dataset file into CXG.
|
Convert a dataset file into CXG.
|
||||||
@@ -107,7 +105,7 @@ def convert_to_cxg(
|
|||||||
use_corpora_schema=not disable_corpora_schema)
|
use_corpora_schema=not disable_corpora_schema)
|
||||||
|
|
||||||
# Get the directory that will hold all the CXG files
|
# Get the directory that will hold all the CXG files
|
||||||
cxg_output_container = get_output_directory(input_file, output_directory, should_overwrite)
|
cxg_output_container = get_output_directory(input_file, output_directory, overwrite)
|
||||||
|
|
||||||
h5ad_data_file.to_cxg(cxg_output_container, sparse_threshold,
|
h5ad_data_file.to_cxg(cxg_output_container, sparse_threshold,
|
||||||
convert_anndata_colors_to_cxg_colors=not disable_custom_colors)
|
convert_anndata_colors_to_cxg_colors=not disable_custom_colors)
|
||||||
@@ -118,14 +116,14 @@ def get_output_directory(input_filename, output_directory, should_overwrite):
|
|||||||
Get the name of the CXG output directory to be created/populated during the dataset conversion.
|
Get the name of the CXG output directory to be created/populated during the dataset conversion.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not path.isdir(output_directory) or (path.isdir(output_directory) and should_overwrite):
|
if output_directory and (not path.isdir(output_directory) or (path.isdir(output_directory) and should_overwrite)):
|
||||||
if output_directory.endswith(".cxg"):
|
if output_directory.endswith(".cxg"):
|
||||||
return output_directory
|
return output_directory
|
||||||
return output_directory + ".cxg"
|
return output_directory + ".cxg"
|
||||||
if path.isdir(output_directory) and not should_overwrite:
|
if output_directory and path.isdir(output_directory) and not should_overwrite:
|
||||||
raise click.BadParameter(
|
raise click.BadParameter(
|
||||||
f"Output directory {output_directory} already exists. If you'd like to overwrite, then run the command "
|
f"Output directory {output_directory} already exists. If you'd like to overwrite, then run the command "
|
||||||
f"with the --overwrite flag."
|
f"with the --overwrite flag."
|
||||||
)
|
)
|
||||||
|
|
||||||
return path.splitext(input_filename)[1] + ".cxg"
|
return path.splitext(input_filename)[0] + ".cxg"
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ def convert_dataframe_to_cxg_array(cxg_container, dataframe_name, dataframe, ind
|
|||||||
schema_hints = {}
|
schema_hints = {}
|
||||||
for column_name, column_values in dataframe.items():
|
for column_name, column_values in dataframe.items():
|
||||||
dtype, hints = get_dtype_and_schema_of_array(column_values)
|
dtype, hints = get_dtype_and_schema_of_array(column_values)
|
||||||
|
|
||||||
value[column_name] = column_values.to_numpy(dtype=dtype)
|
value[column_name] = column_values.to_numpy(dtype=dtype)
|
||||||
if hints:
|
if hints:
|
||||||
schema_hints.update({column_name: hints})
|
schema_hints.update({column_name: hints})
|
||||||
|
|||||||
@@ -37,19 +37,19 @@ def get_dtype_from_dtype(dtype, array_values=None):
|
|||||||
dtype_name = dtype.name
|
dtype_name = dtype.name
|
||||||
dtype_kind = dtype.kind
|
dtype_kind = dtype.kind
|
||||||
|
|
||||||
if dtype == np.float32 or dtype == np.int32:
|
|
||||||
return dtype
|
|
||||||
if dtype_name == "bool":
|
if dtype_name == "bool":
|
||||||
return np.uint8
|
return np.uint8
|
||||||
if dtype_name == "object" and dtype_kind == "O":
|
if dtype_name == "object" and dtype_kind == "O":
|
||||||
return np.unicode
|
return np.unicode
|
||||||
if dtype_name == "category":
|
if dtype_name == "category":
|
||||||
return get_dtype_from_dtype(dtype.categories.dtype, dtype.categories)
|
return get_dtype_from_dtype(dtype.categories.dtype, array_values)
|
||||||
|
|
||||||
if can_cast_to_float32(dtype):
|
|
||||||
return np.float32
|
|
||||||
if can_cast_to_int32(dtype, array_values):
|
if can_cast_to_int32(dtype, array_values):
|
||||||
return np.int32
|
return np.int32
|
||||||
|
if can_cast_to_float32(dtype, array_values):
|
||||||
|
return np.float32
|
||||||
|
if not can_cast_to_float32(dtype, array_values):
|
||||||
|
return np.float64
|
||||||
|
|
||||||
raise TypeError(f"Annotations of type {dtype} are unsupported.")
|
raise TypeError(f"Annotations of type {dtype} are unsupported.")
|
||||||
|
|
||||||
@@ -72,19 +72,43 @@ def get_schema_type_hint_from_dtype(dtype, array_values=None):
|
|||||||
if dtype_name == "category":
|
if dtype_name == "category":
|
||||||
return {"type": "categorical", "categories": dtype.categories.tolist()}
|
return {"type": "categorical", "categories": dtype.categories.tolist()}
|
||||||
|
|
||||||
if can_cast_to_float32(dtype):
|
|
||||||
return {"type": "float32"}
|
|
||||||
if can_cast_to_int32(dtype, array_values):
|
if can_cast_to_int32(dtype, array_values):
|
||||||
return {"type": "int32"}
|
return {"type": "int32"}
|
||||||
|
if can_cast_to_float32(dtype, array_values):
|
||||||
|
return {"type": "float32"}
|
||||||
|
if dtype_kind == "f" and not can_cast_to_float32(dtype, array_values):
|
||||||
|
return {"type": "float64"}
|
||||||
|
|
||||||
raise TypeError(f"Annotations of type {dtype} are unsupported.")
|
raise TypeError(f"Annotations of type {dtype} are unsupported.")
|
||||||
|
|
||||||
|
|
||||||
def can_cast_to_float32(dtype):
|
def can_cast_to_float32(dtype, array_values):
|
||||||
|
"""
|
||||||
|
A dtype can be cast to float32 if it is a float type and converting it to float32 presents the same output as the
|
||||||
|
original values. Note that NaNs fail equality (i.e. np.NaN != np.NaN) so we use np.testing.assert_equal to ensure
|
||||||
|
that the arrays are equal minus NaNs.
|
||||||
|
|
||||||
|
We also handle a special case here where the array is a Series object with integer categorical values AND NaNs.
|
||||||
|
Since NaNs are floating points in numpy, we upcast the integer array to float32.
|
||||||
|
"""
|
||||||
|
|
||||||
if dtype.kind == "f":
|
if dtype.kind == "f":
|
||||||
if not np.can_cast(dtype, np.float32):
|
# Try to convert the array to float32
|
||||||
|
converted_float32_values = array_values.to_numpy(np.float32)
|
||||||
|
original_values = array_values.to_numpy()
|
||||||
|
|
||||||
|
# Verify that the two arrays are equal except for NaNs (which will equate to be unequal).
|
||||||
|
if not ((converted_float32_values != original_values) == np.isnan(original_values)).all():
|
||||||
|
return False
|
||||||
|
|
||||||
|
if dtype != np.float32:
|
||||||
logging.warning(f"Type {dtype.name} will be converted to 32 bit float and may lose precision.")
|
logging.warning(f"Type {dtype.name} will be converted to 32 bit float and may lose precision.")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
if dtype.kind == "O" and array_values.hasnans:
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -94,11 +118,30 @@ def can_cast_to_int32(dtype, array_values=None):
|
|||||||
the higher precision type has values that are entirely within the range of the downcast type.
|
the higher precision type has values that are entirely within the range of the downcast type.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Since a NaN is technically a float, any array that contains NaNs cannot be cast to an integer so immediately
|
||||||
|
# return False.
|
||||||
|
if array_values.hasnans:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# If the array is categorical, then we need to order the array values so that functions min and max that occur
|
||||||
|
# later, can function. They do not function on unordered categories.
|
||||||
|
ordered_array_values = array_values
|
||||||
|
if array_values.dtype.name == "category" and not array_values.cat.ordered:
|
||||||
|
ordered_array_values = array_values.cat.as_ordered()
|
||||||
|
|
||||||
if dtype.kind in ["i", "u"]:
|
if dtype.kind in ["i", "u"]:
|
||||||
if np.can_cast(dtype, np.int32):
|
if np.can_cast(dtype, np.int32):
|
||||||
return True
|
return True
|
||||||
ii32 = np.iinfo(np.int32)
|
ii32 = np.iinfo(np.int32)
|
||||||
if not array_values.empty and (
|
if not ordered_array_values.empty and (
|
||||||
array_values.min() >= ii32.min and array_values.max() <= ii32.max) or array_values.empty:
|
ordered_array_values.min() >= ii32.min and ordered_array_values.max() <= ii32.max) or \
|
||||||
|
ordered_array_values.empty:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def convert_pandas_series_to_numpy(series_to_convert: pd.Series, dtype):
|
||||||
|
if series_to_convert.hasnans and dtype == np.int32:
|
||||||
|
logging.error("Cannot convert a pandas Series object to an integer dtype if it contains NaNs.")
|
||||||
|
|
||||||
|
return series_to_convert.to_numpy(dtype)
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ def serialize_typed_array(builder, source_array, encoding_info):
|
|||||||
def column_encoding(arr):
|
def column_encoding(arr):
|
||||||
column_encoding_type_map = {
|
column_encoding_type_map = {
|
||||||
# array protocol string: ( array_type, as_type )
|
# array protocol string: ( array_type, as_type )
|
||||||
np.dtype(np.float64).str: (TypedArray.TypedArray.Float32Array, np.float32),
|
np.dtype(np.float64).str: (TypedArray.TypedArray.Float64Array, np.float64),
|
||||||
np.dtype(np.float32).str: (TypedArray.TypedArray.Float32Array, np.float32),
|
np.dtype(np.float32).str: (TypedArray.TypedArray.Float32Array, np.float32),
|
||||||
np.dtype(np.float16).str: (TypedArray.TypedArray.Float32Array, np.float32),
|
np.dtype(np.float16).str: (TypedArray.TypedArray.Float32Array, np.float32),
|
||||||
np.dtype(np.int8).str: (TypedArray.TypedArray.Int32Array, np.int32),
|
np.dtype(np.int8).str: (TypedArray.TypedArray.Int32Array, np.int32),
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
import os
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from server.common.utils.type_conversion_utils import get_schema_type_hint_from_dtype
|
import os
|
||||||
from server.common.errors import DatasetAccessError, ConfigurationError
|
import threading
|
||||||
from server.common.utils.utils import path_join
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import tiledb
|
||||||
|
from server_timing import Timing as ServerTiming
|
||||||
|
|
||||||
|
import server.compute.diffexp_cxg as diffexp_cxg
|
||||||
from server.common.constants import Axis
|
from server.common.constants import Axis
|
||||||
|
from server.common.errors import DatasetAccessError, ConfigurationError
|
||||||
|
from server.common.immutable_kvcache import ImmutableKVCache
|
||||||
|
from server.common.utils.type_conversion_utils import get_schema_type_hint_from_dtype
|
||||||
|
from server.common.utils.utils import path_join
|
||||||
from server.data_common.data_adaptor import DataAdaptor
|
from server.data_common.data_adaptor import DataAdaptor
|
||||||
from server.data_common.fbs.matrix import encode_matrix_fbs
|
from server.data_common.fbs.matrix import encode_matrix_fbs
|
||||||
from server.data_cxg.cxg_util import pack_selector_from_mask
|
from server.data_cxg.cxg_util import pack_selector_from_mask
|
||||||
import server.compute.diffexp_cxg as diffexp_cxg
|
|
||||||
from server.common.immutable_kvcache import ImmutableKVCache
|
|
||||||
import tiledb
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
from server_timing import Timing as ServerTiming
|
|
||||||
import threading
|
|
||||||
|
|
||||||
|
|
||||||
class CxgAdaptor(DataAdaptor):
|
class CxgAdaptor(DataAdaptor):
|
||||||
|
|
||||||
# TODO: The tiledb context parameters should be a configuration option
|
# TODO: The tiledb context parameters should be a configuration option
|
||||||
tiledb_ctx = tiledb.Ctx(
|
tiledb_ctx = tiledb.Ctx(
|
||||||
{"sm.tile_cache_size": 8 * 1024 * 1024 * 1024, "sm.num_reader_threads": 32, "vfs.s3.region": "us-east-1"}
|
{"sm.tile_cache_size": 8 * 1024 * 1024 * 1024, "sm.num_reader_threads": 32, "vfs.s3.region": "us-east-1"}
|
||||||
@@ -337,32 +338,6 @@ class CxgAdaptor(DataAdaptor):
|
|||||||
raise DatasetAccessError("cxg matrix missing embeddings")
|
raise DatasetAccessError("cxg matrix missing embeddings")
|
||||||
return embeddings
|
return embeddings
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_col_type(attr, schema_hints={}):
|
|
||||||
type_hint = schema_hints.get(attr.name, {})
|
|
||||||
dtype = attr.dtype
|
|
||||||
schema = {}
|
|
||||||
# type hints take precedence
|
|
||||||
if "type" in type_hint:
|
|
||||||
schema["type"] = type_hint["type"]
|
|
||||||
elif dtype == np.float32:
|
|
||||||
schema["type"] = "float32"
|
|
||||||
elif dtype == np.int32:
|
|
||||||
schema["type"] = "int32"
|
|
||||||
elif dtype == np.bool_:
|
|
||||||
schema["type"] = "boolean"
|
|
||||||
elif dtype == np.str:
|
|
||||||
schema["type"] = "string"
|
|
||||||
elif dtype == "category":
|
|
||||||
schema["type"] = "categorical"
|
|
||||||
schema["categories"] = dtype.categories.tolist()
|
|
||||||
else:
|
|
||||||
raise TypeError(f"Annotations of type {dtype} are unsupported.")
|
|
||||||
|
|
||||||
if schema["type"] == "categorical" and "categories" in schema_hints:
|
|
||||||
schema["categories"] = schema_hints["categories"]
|
|
||||||
return schema
|
|
||||||
|
|
||||||
def _get_schema(self):
|
def _get_schema(self):
|
||||||
if self.schema:
|
if self.schema:
|
||||||
return self.schema
|
return self.schema
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
from time import time
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pandas import Series, DataFrame
|
from pandas import Series, DataFrame
|
||||||
|
|
||||||
from server.common.utils.type_conversion_utils import can_cast_to_float32, can_cast_to_int32, get_dtype_of_array, \
|
from server.common.utils.type_conversion_utils import can_cast_to_float32, can_cast_to_int32, get_dtype_of_array, \
|
||||||
get_schema_type_hint_of_array, get_dtypes_and_schemas_of_dataframe
|
get_schema_type_hint_of_array, get_dtypes_and_schemas_of_dataframe, convert_pandas_series_to_numpy
|
||||||
|
|
||||||
|
|
||||||
class TestTypeConversionUtils(unittest.TestCase):
|
class TestTypeConversionUtils(unittest.TestCase):
|
||||||
@@ -13,28 +14,49 @@ class TestTypeConversionUtils(unittest.TestCase):
|
|||||||
def test__can_cast_to_float32__string_is_false(self):
|
def test__can_cast_to_float32__string_is_false(self):
|
||||||
array_to_convert = Series(data=["1", "2", "3"], dtype=str)
|
array_to_convert = Series(data=["1", "2", "3"], dtype=str)
|
||||||
|
|
||||||
can_cast = can_cast_to_float32(array_to_convert.dtype)
|
can_cast = can_cast_to_float32(array_to_convert.dtype, array_to_convert)
|
||||||
|
|
||||||
self.assertFalse(can_cast)
|
self.assertFalse(can_cast)
|
||||||
|
|
||||||
def test__can_cast_to_float32__int_is_true_warning_outputted(self):
|
def test__can_cast_to_float32__float64_is_true_warning_outputted(self):
|
||||||
array_to_convert = Series(data=[1, 2, 3], dtype=np.dtype(np.float64))
|
array_to_convert = Series(data=[1, 2, 3], dtype=np.dtype(np.float64))
|
||||||
|
|
||||||
with self.assertLogs(level="WARN") as logger:
|
with self.assertLogs(level="WARN") as logger:
|
||||||
can_cast = can_cast_to_float32(array_to_convert.dtype)
|
can_cast = can_cast_to_float32(array_to_convert.dtype, array_to_convert)
|
||||||
self.assertIn("may lose precision", logger.output[0])
|
self.assertIn("may lose precision", logger.output[0])
|
||||||
|
|
||||||
self.assertTrue(can_cast)
|
self.assertTrue(can_cast)
|
||||||
|
|
||||||
@patch("logging.warning")
|
@patch("logging.warning")
|
||||||
def test__can_cast_to_float64__int_is_false(self, mock_log_warning):
|
def test__can_cast_to_float32__float32_is_false(self, mock_log_warning):
|
||||||
array_to_convert = Series(data=[1, 2, 3], dtype=np.dtype(np.float32))
|
array_to_convert = Series(data=[1, 2, 3], dtype=np.dtype(np.float32))
|
||||||
|
|
||||||
can_cast = can_cast_to_float32(array_to_convert.dtype)
|
can_cast = can_cast_to_float32(array_to_convert.dtype, array_to_convert)
|
||||||
|
|
||||||
self.assertTrue(can_cast)
|
self.assertTrue(can_cast)
|
||||||
assert not mock_log_warning.called
|
assert not mock_log_warning.called
|
||||||
|
|
||||||
|
def test__can_cast_to_float32__categorical_float64_is_false(self):
|
||||||
|
array_to_convert = Series(data=[1.1, 2.2, 3.3], dtype="category")
|
||||||
|
|
||||||
|
can_cast = can_cast_to_float32(array_to_convert.dtype, array_to_convert)
|
||||||
|
|
||||||
|
self.assertFalse(can_cast)
|
||||||
|
|
||||||
|
def test__can_cast_to_float32__categorical_int64_with_nans_is_true(self):
|
||||||
|
array_to_convert = Series(data=[1, 2, np.NaN], dtype="category")
|
||||||
|
|
||||||
|
can_cast = can_cast_to_float32(array_to_convert.dtype, array_to_convert)
|
||||||
|
|
||||||
|
self.assertTrue(can_cast)
|
||||||
|
|
||||||
|
def test__can_cast_to_float_32__float_32_with_nans_is_true(self):
|
||||||
|
array_to_convert = Series(data=[1, 2, np.NaN], dtype=np.dtype(np.float32))
|
||||||
|
|
||||||
|
can_cast = can_cast_to_float32(array_to_convert.dtype, array_to_convert)
|
||||||
|
|
||||||
|
self.assertTrue(can_cast)
|
||||||
|
|
||||||
def test__can_cast_to_int32__string_is_false(self):
|
def test__can_cast_to_int32__string_is_false(self):
|
||||||
array_to_convert = Series(data=["1", "2", "3"], dtype=str)
|
array_to_convert = Series(data=["1", "2", "3"], dtype=str)
|
||||||
|
|
||||||
@@ -63,6 +85,13 @@ class TestTypeConversionUtils(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertFalse(can_cast)
|
self.assertFalse(can_cast)
|
||||||
|
|
||||||
|
def test__can_cast_to_int32__int64_with_nans_is_false(self):
|
||||||
|
array_to_convert = Series(data=[np.NaN, "2", "3"], dtype="category")
|
||||||
|
|
||||||
|
can_cast = can_cast_to_int32(array_to_convert.dtype, array_to_convert)
|
||||||
|
|
||||||
|
self.assertFalse(can_cast)
|
||||||
|
|
||||||
def test__get_dtype_of_array__supported_dtypes_return_as_expected(self):
|
def test__get_dtype_of_array__supported_dtypes_return_as_expected(self):
|
||||||
types = [np.float32, np.int32, np.bool_, str]
|
types = [np.float32, np.int32, np.bool_, str]
|
||||||
expected_dtypes = [np.float32, np.int32, np.uint8, np.unicode]
|
expected_dtypes = [np.float32, np.int32, np.uint8, np.unicode]
|
||||||
@@ -73,6 +102,40 @@ class TestTypeConversionUtils(unittest.TestCase):
|
|||||||
array = Series(data=[], dtype=types[test_type_index])
|
array = Series(data=[], dtype=types[test_type_index])
|
||||||
self.assertEqual(get_dtype_of_array(array), expected_dtypes[test_type_index])
|
self.assertEqual(get_dtype_of_array(array), expected_dtypes[test_type_index])
|
||||||
|
|
||||||
|
def test__get_dtype_of_array__categories_return_as_expected(self):
|
||||||
|
array = Series(data=["a", "b", "c"], dtype="category")
|
||||||
|
expected_dtype = np.unicode
|
||||||
|
|
||||||
|
actual_dtype = get_dtype_of_array(array)
|
||||||
|
|
||||||
|
self.assertEqual(expected_dtype, actual_dtype)
|
||||||
|
|
||||||
|
def test__get_dtype_of_array__unordered_integer_categories_return_as_expected(self):
|
||||||
|
array = Series(data=[2, 3, 1, 3, 1, 2], dtype="category")
|
||||||
|
expected_dtype = np.int32
|
||||||
|
|
||||||
|
actual_dtype = get_dtype_of_array(array)
|
||||||
|
|
||||||
|
self.assertEqual(expected_dtype, actual_dtype)
|
||||||
|
|
||||||
|
def test__get_dtype_of_array__castable_dtypes_return_as_expected(self):
|
||||||
|
types = [np.float64, np.int64]
|
||||||
|
expected_dtypes = [np.float32, np.int32]
|
||||||
|
|
||||||
|
for test_type_index in range(len(types)):
|
||||||
|
with self.subTest(f"Testing get_dtype_of_array with castable type {types[test_type_index].__name__}",
|
||||||
|
i=test_type_index):
|
||||||
|
array = Series(data=[], dtype=types[test_type_index])
|
||||||
|
self.assertEqual(get_dtype_of_array(array), expected_dtypes[test_type_index])
|
||||||
|
|
||||||
|
def test__get_dtype_of_array__unsupported_type_raises_exception(self):
|
||||||
|
unsupported_array = Series(list([time() for _ in range(2)]), dtype="datetime64[ns]")
|
||||||
|
|
||||||
|
with self.assertRaises(TypeError) as exception_context:
|
||||||
|
get_dtype_of_array(unsupported_array)
|
||||||
|
|
||||||
|
self.assertIn("unsupported", str(exception_context.exception))
|
||||||
|
|
||||||
def test__get_schema_type_hint_of_array__supported_dtypes_return_as_expected(self):
|
def test__get_schema_type_hint_of_array__supported_dtypes_return_as_expected(self):
|
||||||
types = [np.float32, np.int32, np.bool_, str]
|
types = [np.float32, np.int32, np.bool_, str]
|
||||||
expected_schema_hints = [{"type": "float32"}, {"type": "int32"}, {"type": "boolean"}, {"type": "string"}]
|
expected_schema_hints = [{"type": "float32"}, {"type": "int32"}, {"type": "boolean"}, {"type": "string"}]
|
||||||
@@ -83,14 +146,6 @@ class TestTypeConversionUtils(unittest.TestCase):
|
|||||||
array = Series(data=[], dtype=types[test_type_index])
|
array = Series(data=[], dtype=types[test_type_index])
|
||||||
self.assertEqual(get_schema_type_hint_of_array(array), expected_schema_hints[test_type_index])
|
self.assertEqual(get_schema_type_hint_of_array(array), expected_schema_hints[test_type_index])
|
||||||
|
|
||||||
def test__get_dtype_of_array__categories_return_as_expected(self):
|
|
||||||
array = Series(data=["a", "b", "c"], dtype="category")
|
|
||||||
expected_dtype = np.unicode
|
|
||||||
|
|
||||||
actual_dtype = get_dtype_of_array(array)
|
|
||||||
|
|
||||||
self.assertEqual(expected_dtype, actual_dtype)
|
|
||||||
|
|
||||||
def test__get_schema_type_hint_of_array__categories_return_as_expected(self):
|
def test__get_schema_type_hint_of_array__categories_return_as_expected(self):
|
||||||
array = Series(data=["a", "b", "b"], dtype="category")
|
array = Series(data=["a", "b", "b"], dtype="category")
|
||||||
expected_schema_hint = {"type": "categorical", "categories": ["a", "b"]}
|
expected_schema_hint = {"type": "categorical", "categories": ["a", "b"]}
|
||||||
@@ -99,16 +154,6 @@ class TestTypeConversionUtils(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(expected_schema_hint, actual_schema_hint)
|
self.assertEqual(expected_schema_hint, actual_schema_hint)
|
||||||
|
|
||||||
def test__get_dtype_of_array__castable_dtypes_return_as_expected(self):
|
|
||||||
types = [np.float64, np.int64]
|
|
||||||
expected_dtypes = [np.float32, np.int32]
|
|
||||||
|
|
||||||
for test_type_index in range(len(types)):
|
|
||||||
with self.subTest(f"Testing get_dtype_of_array with castable type {types[test_type_index].__name__}",
|
|
||||||
i=test_type_index):
|
|
||||||
array = Series(data=[], dtype=types[test_type_index])
|
|
||||||
self.assertEqual(get_dtype_of_array(array), expected_dtypes[test_type_index])
|
|
||||||
|
|
||||||
def test__get_schema_type_hint_of_array__castable_dtypes_return_as_expected(self):
|
def test__get_schema_type_hint_of_array__castable_dtypes_return_as_expected(self):
|
||||||
types = [np.float64, np.int64]
|
types = [np.float64, np.int64]
|
||||||
expected_schema_hints = [{"type": "float32"}, {"type": "int32"}]
|
expected_schema_hints = [{"type": "float32"}, {"type": "int32"}]
|
||||||
@@ -133,3 +178,28 @@ class TestTypeConversionUtils(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(expected_data_types_dict, actual_dataframe_data_types)
|
self.assertEqual(expected_data_types_dict, actual_dataframe_data_types)
|
||||||
self.assertEqual(expected_schema_type_hints_dict, actual_dataframe_schema_type_hints)
|
self.assertEqual(expected_schema_type_hints_dict, actual_dataframe_schema_type_hints)
|
||||||
|
|
||||||
|
def test__convert_pandas_series_to_numpy__categorical_float64_to_float64_with_nans(self):
|
||||||
|
expected_float_array = np.array([1.1, 2.2, np.NaN], dtype=np.float64)
|
||||||
|
float_series = Series(data=[1.1, 2.2, np.NaN], dtype="category")
|
||||||
|
|
||||||
|
actual_float_array = convert_pandas_series_to_numpy(float_series, np.float64)
|
||||||
|
|
||||||
|
np.testing.assert_equal(expected_float_array, actual_float_array)
|
||||||
|
|
||||||
|
def test__convert_pandas_series_to_numpy__float64_to_float64(self):
|
||||||
|
expected_float_array = np.array([1.1, 2.2], dtype=np.float64)
|
||||||
|
float_series = Series(data=[1.1, 2.2], dtype=np.dtype(np.float64))
|
||||||
|
|
||||||
|
actual_float_array = convert_pandas_series_to_numpy(float_series, np.float64)
|
||||||
|
|
||||||
|
np.testing.assert_equal(expected_float_array, actual_float_array)
|
||||||
|
|
||||||
|
def test__convert_pandas_series_to_numpy__int64_to_int32_with_nans_throws_error(self):
|
||||||
|
int_series = Series(data=[1, 2, np.NaN], dtype="category")
|
||||||
|
|
||||||
|
with self.assertLogs(level="ERROR") as logger:
|
||||||
|
convert_pandas_series_to_numpy(int_series, np.int32)
|
||||||
|
|
||||||
|
self.assertIn("Cannot convert a pandas Series object to an integer dtype if it contains NaNs",
|
||||||
|
logger.output[0])
|
||||||
|
|||||||
Reference in New Issue
Block a user