[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org)
![PyPI - Downloads](https://img.shields.io/pypi/dm/pandas-categorical)
![PyPI](https://img.shields.io/pypi/v/pandas-categorical?label=pypi%20pandas-categorical)
![CI - Test](https://github.com/loskost/pandas-categorical/actions/workflows/testing_package.yml/badge.svg)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
The package contains just a few features that make using pandas categorical types easier to use.
The main purpose of using categorical types is to reduce RAM consumption when working with large datasets. Experience shows that on average, there is a decrease of 2 times (for datasets of several GB, this is very significant). The full justification of the reasons and examples will be given below.
# Quickstart
```
pip install pandas-categorical
```
```python
import pandas as pd
import pandas-categorical as pdc
```
```python
df.astype('category') -> pdc.cat_astype(df, ...)
pd.concat() -> pdc.concat_categorical()
pd.merge() -> pdc.merge_categorical()
df.groupby(...) -> df.groupby(..., observed=True)
```
## cat_astype
```python
df = pd.read_csv("path_to_dataframe.csv")
SUB_DTYPES = {
'cat_col_with_int_values': int,
'cat_col_with_string_values': 'string',
'ordered_cat_col_with_bool_values': bool,
}
pdc.cat_astype(
data=df,
cat_cols=SUB_DTYPES.keys(),
sub_dtypes=SUB_DTYPES,
ordered_cols=['ordered_cat_col_with_bool_values']
)
```
## concat_categorical
```python
df_1 = ... # dataset with some categorical columns
df_2 = ... # dataset with some categorical columns (categories are not equals)
df_res = pdc.concat_categorical((df_1, df_2), axis=0, ignore_index=True)
```
## merge_categorical
```python
df_1 = ... # dataset with some categorical columns
df_2 = ... # dataset with some categorical columns (categories are not equals)
df_res = pdc.merge_categorical(df_1, df_2, on=['cat_col_1', 'cat_col_2'])
```
# A bit of theory
The advantages are discussed in detail in the articles [here](https://towardsdatascience.com/staying-sane-while-adopting-pandas-categorical-datatypes-78dbd19dcd8a), [here](https://towardsdatascience.com/pandas-groupby-aggregate-transform-filter-c95ba3444bbb) and [here](https://pandas.pydata.org/docs/user_guide/categorical.html).
The categorical type implies the presence of a certain set of unique values in this column, which are often repeated. By reducing the copying of identical values, it is possible to reduce the size of the column (the larger the dataset, the more likely repetitions are). By default, categories (unique values) have no order. That is, they are not comparable to each other. It is possible to make them ordered.
Pandas already has everything for this (for example, `.astype(’category’)`). However, standard methods, in my opinion, require a high entry threshold and therefore are rarely used.
Let's try to outline a number of problems and ways to solve them.
## 1. Categorical types are easy to lose
Suppose you want to connect two datasets into one using `pd.concat(..., axis=0)`. Datasets contain columns with categorical types.
If the column categories of the source datasets are different, then `pandas` it does not combine multiple values, but simply resets their default type (for example, `object`, `int`, ...).
In other words,
$$\textcolor{red}{category1} + \textcolor{green}{category2} = object$$
$$\textcolor{red}{category1} + \textcolor{red}{category1} = \textcolor{red}{category1}$$
But we would like to observe a different behavior:
$$\textcolor{red}{category1} + \textcolor{green}{category2} = \textcolor{blue}{category3}$$
$$(\textcolor{blue}{category3} = \textcolor{red}{category1} \cup \textcolor{green}{category2})$$
As a result, you need to monitor the reduction of categories before actions such as `merge` or `concat`.
## 2. Categories type control
When you do a type conversion
```python
df['col_name'] = df['col_name'].astype('category')
```
the type of categories is equal to the type of the source column.
But, if you want to change the type of categories, you probably want to write something like
```python
df['col_name'] = df['col_name'].astype('some_new_type').astype('category')
```
That is, you will temporarily lose the categorical type (respectively, and the advantages of memory).
By the way, the usual way of control
```python
df.dtypes
```
does not display information about the type of categories themselves. You will only see only `category` next to the desired column.
## 3. Unused values
Suppose you have filtered the dataset. At the same time, the actual set of values of categorical columns could decrease, but the data type will not be changed.
This can negatively affect, for example, when working with `groupby` on such a column. As a result, grouping will also occur by unused categories. To prevent this from happening, you need to specify the `observed=True` parameter.
For example,
```python
df.groupby(['cat_col_1', 'cat_col_2'], observed=True).agg('mean')
```
## 4. Ordered categories
There is a understandable instruction for converting a column type to a categorical (unordered) one
```python
df[col] = df[col].astype('category')
```
But there is no similar command to convert to an ordered categorical type.
There are two non-obvious ways:
```python
df[col] = df[col].astype('category').cat.as_ordered()
```
Or
```python
df[col] = df[col].astype(pd.CategoricalDtype(ordered=True))
```
## 5. Minimum copying
To process large datasets, you need to minimize the creation of copies of even its parts. Therefore, the functions from this package do the transformations in place.
## 6. Data storage in parquet format
When using `pd.to_parquet(path, engine='pyarrow')` and `pd.read_parque(path, engine='pyarrow')`categorical types of some columns can be reset to normal. To solve this problem, you can use `engine='fastparquet'`.
Note 1: `fastparquet` usually runs a little slower than `pyarrow`.
Note 2: `pyarrow` and `fastparquet` cannot be used together (for example, save by one and read by the other). This can lead to data loss.
```python
import pandas as pd
df = pd.DataFrame(
{
"Date": pd.date_range('2023-01-01', periods=10),
"Object": ["a"]*5+["b"]+["c"]*4,
"Int": [1, 1, 1, 2, 3, 1, 2, 4, 3, 2],
"Float": [1.1]*5+[2.2]*5,
}
)
print(df.dtypes)
df = df.astype('category')
print(df.dtypes)
df.to_parquet('test.parquet', engine='pyarrow')
df = pd.read_parquet('test.parquet', engine='pyarrow')
print(df.dtypes)
```
Output:
```
Date datetime64[ns]
Object object
Int int64
Float float64
dtype: object
Date category
Object category
Int category
Float category
dtype: object
Date datetime64[ns]
Object category
Int int64
Float float64
dtype: object
```
# Examples
- [Jupiter notebook with examples](https://www.kaggle.com/code/loskost/problems-of-pandas-categorical-dtypes) of problems is posted on kaggle. A copy can be found in the `examples/` folder.
- [Jupiter notebook with solution](https://www.kaggle.com/code/loskost/problems-of-pandas-categorical-dtypes-solution) of problems. A copy can be found in the `examples/` folder.
- Also, usage examples can be found in the tests folder.
# Remarks
1. Processing of categorical indexes has not yet been implemented.
2. In the future, the function `pdc.join_categorical()` will appear.
3. The `cat_astype` function was designed so that the type information could be redundant (for example, it is specified for all possible column names in the project at once). In the future, it will be possible to set default values for this function.
# Links
1. [Official pandas documentation](https://pandas.pydata.org/docs/user_guide/categorical.html).
2. https://towardsdatascience.com/staying-sane-while-adopting-pandas-categorical-datatypes-78dbd19dcd8a
3. https://towardsdatascience.com/pandas-groupby-aggregate-transform-filter-c95ba3444bbb
4. [The source of the idea](https://stackoverflow.com/questions/45639350/retaining-categorical-dtype-upon-dataframe-concatenation) that I wanted to develop.
Raw data
{
"_id": null,
"home_page": "https://github.com/loskost/pandas-categorical",
"name": "pandas-categorical",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.7,<3.12",
"maintainer_email": "",
"keywords": "data,datascience,pandas",
"author": "KonstantinLosev",
"author_email": "loskost@ya.ru",
"download_url": "https://files.pythonhosted.org/packages/cf/c2/38885723a482d6b54aa3026578b25fd3dd460c66e8c9fe19ece522f04e9b/pandas_categorical-1.1.0.tar.gz",
"platform": null,
"description": "[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org)\n![PyPI - Downloads](https://img.shields.io/pypi/dm/pandas-categorical)\n![PyPI](https://img.shields.io/pypi/v/pandas-categorical?label=pypi%20pandas-categorical)\n![CI - Test](https://github.com/loskost/pandas-categorical/actions/workflows/testing_package.yml/badge.svg)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\n\nThe package contains just a few features that make using pandas categorical types easier to use.\nThe main purpose of using categorical types is to reduce RAM consumption when working with large datasets. Experience shows that on average, there is a decrease of 2 times (for datasets of several GB, this is very significant). The full justification of the reasons and examples will be given below.\n# Quickstart\n\n```\npip install pandas-categorical\n```\n\n```python\nimport pandas as pd\nimport pandas-categorical as pdc\n```\n\n```python\ndf.astype('category') -> pdc.cat_astype(df, ...)\npd.concat() -> pdc.concat_categorical()\npd.merge() -> pdc.merge_categorical()\ndf.groupby(...) -> df.groupby(..., observed=True)\n```\n## cat_astype\n\n```python\ndf = pd.read_csv(\"path_to_dataframe.csv\")\n\nSUB_DTYPES = {\n\t'cat_col_with_int_values': int,\n\t'cat_col_with_string_values': 'string',\n\t'ordered_cat_col_with_bool_values': bool,\n}\npdc.cat_astype(\n\tdata=df,\n\tcat_cols=SUB_DTYPES.keys(),\n\tsub_dtypes=SUB_DTYPES,\n\tordered_cols=['ordered_cat_col_with_bool_values']\n)\n```\n\n## concat_categorical\n\n```python\ndf_1 = ... # dataset with some categorical columns\ndf_2 = ... # dataset with some categorical columns (categories are not equals)\n\ndf_res = pdc.concat_categorical((df_1, df_2), axis=0, ignore_index=True)\n```\n\n## merge_categorical\n\n```python\ndf_1 = ... # dataset with some categorical columns\ndf_2 = ... # dataset with some categorical columns (categories are not equals)\n\ndf_res = pdc.merge_categorical(df_1, df_2, on=['cat_col_1', 'cat_col_2'])\n```\n\n# A bit of theory\n\nThe advantages are discussed in detail in the articles [here](https://towardsdatascience.com/staying-sane-while-adopting-pandas-categorical-datatypes-78dbd19dcd8a), [here](https://towardsdatascience.com/pandas-groupby-aggregate-transform-filter-c95ba3444bbb) and [here](https://pandas.pydata.org/docs/user_guide/categorical.html).\n\nThe categorical type implies the presence of a certain set of unique values in this column, which are often repeated. By reducing the copying of identical values, it is possible to reduce the size of the column (the larger the dataset, the more likely repetitions are). By default, categories (unique values) have no order. That is, they are not comparable to each other. It is possible to make them ordered.\n\nPandas already has everything for this (for example, `.astype(\u2019category\u2019)`). However, standard methods, in my opinion, require a high entry threshold and therefore are rarely used.\n\nLet's try to outline a number of problems and ways to solve them.\n\n## 1. Categorical types are easy to lose\n\nSuppose you want to connect two datasets into one using `pd.concat(..., axis=0)`. Datasets contain columns with categorical types.\nIf the column categories of the source datasets are different, then `pandas` it does not combine multiple values, but simply resets their default type (for example, `object`, `int`, ...).\nIn other words,\n$$\\textcolor{red}{category1} + \\textcolor{green}{category2} = object$$\n$$\\textcolor{red}{category1} + \\textcolor{red}{category1} = \\textcolor{red}{category1}$$\nBut we would like to observe a different behavior:\n$$\\textcolor{red}{category1} + \\textcolor{green}{category2} = \\textcolor{blue}{category3}$$\n$$(\\textcolor{blue}{category3} = \\textcolor{red}{category1} \\cup \\textcolor{green}{category2})$$\nAs a result, you need to monitor the reduction of categories before actions such as `merge` or `concat`.\n## 2. Categories type control\n\nWhen you do a type conversion\n```python\ndf['col_name'] = df['col_name'].astype('category')\n```\nthe type of categories is equal to the type of the source column.\nBut, if you want to change the type of categories, you probably want to write something like\n```python\ndf['col_name'] = df['col_name'].astype('some_new_type').astype('category')\n\n```\nThat is, you will temporarily lose the categorical type (respectively, and the advantages of memory).\nBy the way, the usual way of control\n```python\ndf.dtypes\n```\ndoes not display information about the type of categories themselves. You will only see only `category` next to the desired column.\n\n## 3. Unused values\n\nSuppose you have filtered the dataset. At the same time, the actual set of values of categorical columns could decrease, but the data type will not be changed.\nThis can negatively affect, for example, when working with `groupby` on such a column. As a result, grouping will also occur by unused categories. To prevent this from happening, you need to specify the `observed=True` parameter.\nFor example,\n```python\ndf.groupby(['cat_col_1', 'cat_col_2'], observed=True).agg('mean')\n```\n\n## 4. Ordered categories\n\nThere is a understandable instruction for converting a column type to a categorical (unordered) one\n```python\ndf[col] = df[col].astype('category')\n```\nBut there is no similar command to convert to an ordered categorical type.\nThere are two non-obvious ways:\n```python\ndf[col] = df[col].astype('category').cat.as_ordered()\n```\nOr\n```python\ndf[col] = df[col].astype(pd.CategoricalDtype(ordered=True))\n```\n\n\n## 5. Minimum copying\n\nTo process large datasets, you need to minimize the creation of copies of even its parts. Therefore, the functions from this package do the transformations in place.\n\n\n## 6. Data storage in parquet format\n\nWhen using `pd.to_parquet(path, engine='pyarrow')` and `pd.read_parque(path, engine='pyarrow')`categorical types of some columns can be reset to normal. To solve this problem, you can use `engine='fastparquet'`. \n\nNote 1: `fastparquet` usually runs a little slower than `pyarrow`.\n\nNote 2: `pyarrow` and `fastparquet` cannot be used together (for example, save by one and read by the other). This can lead to data loss.\n\n```python\nimport pandas as pd\n\n\ndf = pd.DataFrame(\n\t{\n\t\t\"Date\": pd.date_range('2023-01-01', periods=10),\n\t\t\"Object\": [\"a\"]*5+[\"b\"]+[\"c\"]*4,\n\t\t\"Int\": [1, 1, 1, 2, 3, 1, 2, 4, 3, 2],\n\t\t\"Float\": [1.1]*5+[2.2]*5,\n\t}\n)\n\nprint(df.dtypes)\ndf = df.astype('category')\nprint(df.dtypes)\ndf.to_parquet('test.parquet', engine='pyarrow')\ndf = pd.read_parquet('test.parquet', engine='pyarrow')\nprint(df.dtypes)\n```\nOutput:\n```\nDate datetime64[ns]\nObject object\nInt int64\nFloat float64\ndtype: object\n\nDate category\nObject category\nInt category\nFloat category\ndtype: object\n\nDate datetime64[ns]\nObject category\nInt int64\nFloat float64\ndtype: object\n```\n\n# Examples\n\n- [Jupiter notebook with examples](https://www.kaggle.com/code/loskost/problems-of-pandas-categorical-dtypes) of problems is posted on kaggle. A copy can be found in the `examples/` folder.\n- [Jupiter notebook with solution](https://www.kaggle.com/code/loskost/problems-of-pandas-categorical-dtypes-solution) of problems. A copy can be found in the `examples/` folder.\n- Also, usage examples can be found in the tests folder.\n\n# Remarks\n\n1. Processing of categorical indexes has not yet been implemented.\n2. In the future, the function `pdc.join_categorical()` will appear.\n3. The `cat_astype` function was designed so that the type information could be redundant (for example, it is specified for all possible column names in the project at once). In the future, it will be possible to set default values for this function.\n# Links\n\n1. [Official pandas documentation](https://pandas.pydata.org/docs/user_guide/categorical.html).\n2. https://towardsdatascience.com/staying-sane-while-adopting-pandas-categorical-datatypes-78dbd19dcd8a\n3. https://towardsdatascience.com/pandas-groupby-aggregate-transform-filter-c95ba3444bbb\n4. [The source of the idea](https://stackoverflow.com/questions/45639350/retaining-categorical-dtype-upon-dataframe-concatenation) that I wanted to develop.\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "A set of functions for working with categorical columns in pandas",
"version": "1.1.0",
"project_urls": {
"Homepage": "https://github.com/loskost/pandas-categorical",
"Repository": "https://github.com/loskost/pandas-categorical"
},
"split_keywords": [
"data",
"datascience",
"pandas"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "a093c94a83e0c4fd9ecc64ea4ff2b0b98790ec64414f79f41045869439605012",
"md5": "163741df9b1f6360199502c2b6aebf6f",
"sha256": "8dea64e7549d36b140d290ee9ae6af35844ecd9caeef8b854e87fdf8f05489cb"
},
"downloads": -1,
"filename": "pandas_categorical-1.1.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "163741df9b1f6360199502c2b6aebf6f",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.7,<3.12",
"size": 7333,
"upload_time": "2024-02-03T12:04:33",
"upload_time_iso_8601": "2024-02-03T12:04:33.628320Z",
"url": "https://files.pythonhosted.org/packages/a0/93/c94a83e0c4fd9ecc64ea4ff2b0b98790ec64414f79f41045869439605012/pandas_categorical-1.1.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "cfc238885723a482d6b54aa3026578b25fd3dd460c66e8c9fe19ece522f04e9b",
"md5": "107ac0c4d4afe7d302dde839a6273ca3",
"sha256": "ab7adc335fb1ad316aded08a1ee37a0b7b1a6a53c5a714bebe7153e45ca8f128"
},
"downloads": -1,
"filename": "pandas_categorical-1.1.0.tar.gz",
"has_sig": false,
"md5_digest": "107ac0c4d4afe7d302dde839a6273ca3",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.7,<3.12",
"size": 6578,
"upload_time": "2024-02-03T12:04:37",
"upload_time_iso_8601": "2024-02-03T12:04:37.794666Z",
"url": "https://files.pythonhosted.org/packages/cf/c2/38885723a482d6b54aa3026578b25fd3dd460c66e8c9fe19ece522f04e9b/pandas_categorical-1.1.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-02-03 12:04:37",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "loskost",
"github_project": "pandas-categorical",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "pandas-categorical"
}