According to documentation, fastexcel maps directly to the arrow format which doesn't allow duplicated column names.
For this reason, fastexcel turns duplicated column into unique by appending _1, _2 etc. to them.
This is highly inconvenient for any data analysis where _X might be real key, and thus it might be impossible to know if _1 was added because of duplicated columns, or _1 is real data.
On top of this, I haven't found any documentation specifying how fastexcel is even handling duplicated columns.
- How fastexcel deduplicates columns needs to be documented.
- Either allow passing custom make_unique function or expose separator such that column name have form:
f"{column}{sep}{i}"
The only way I have been able to do it is by basically reading the sheet twice, first the header row, transforming it using custom make_unique function, and then replacing the names from second read.
excel = fastexcel.read_excel(path)
header = excel.load_sheet(sheet, header_row=None, n_rows=1).to_polars().row(0)
columns = make_unique(header)
data = excel.load_sheet(sheet).to_polars()
data.columns = columns
My custom make_unique function looks like this:
def make_unique(x: Iterable, separator="_DUPLICATED_", none="_UNNAMED_"):
"""
Make iterable unique by adding separator {separator}{n} to all duplicated names,
where `n` is number of seen duplications starting (present excluded).
In additional to this cleanup, `None` variables are replaced with `f"{none}{i}"`
where `i` is column index, and values are coerced to string with whitespace excluded.
Example:
x = ["a ", 1, "a", "a", None]
y = make_unique(x, "")
# ["a", "1", "a1", "a2", "_UNNAMED_4"]
"""
seen: dict[str, int] = {}
y = []
for i, item in enumerate(x):
if item is None or not str(item).strip():
item = f"{none}{i}"
else:
item = str(item).strip()
if item in seen:
seen[item] += 1
y.append(f"{item}{separator}{seen[item]}")
else:
seen[item] = 0
y.append(item)
return y
_DUPLICATED_ and _UNNAMED_ might look ugly, but they have much smaller chance to exist in real data. And these are configurable, so if user needs something else, they can configure it themselves.
Consider comparison with R's tidyverse workhorse tibble:
#' @param .name_repair Treatment of problematic column names:
#' * `"minimal"`: No name repair or checks, beyond basic existence,
#' * `"unique"`: Make sure names are unique and not empty,
#' * `"check_unique"`: (default value), no name repair, but check they are
#' `unique`,
#' * `"universal"`: Make the names `unique` and syntactic
#' * `"unique_quiet"`: Same as `"unique"`, but "quiet"
#' * `"universal_quiet"`: Same as `"universal"`, but "quiet"
#' * a function: apply custom name repair (e.g., `.name_repair = make.names`
#' for names in the style of base R).
#' * A purrr-style anonymous function, see [rlang::as_function()]
#'
#' This argument is passed on as `repair` to [vctrs::vec_as_names()].
#' See there for more details on these terms and the strategies used
#' to enforce them.
or a similar option, though less convenient, in R base::data.frame
* check.names: logical. If TRUE then the names of the variables in the data frame are checked to ensure that they are syntactically valid variable names and are not duplicated. If necessary they are adjusted (by [make.names](https://www.rdocumentation.org/link/make.names?package=base&version=3.6.2)) so that they are.
in which case user can toggle check.names=FALSE and then fix them themselves (which is possible with data.frames since they are just arrays of columns and unique names are not required)
According to documentation, fastexcel maps directly to the arrow format which doesn't allow duplicated column names.
For this reason, fastexcel turns duplicated column into unique by appending
_1,_2etc. to them.This is highly inconvenient for any data analysis where
_Xmight be real key, and thus it might be impossible to know if_1was added because of duplicated columns, or_1is real data.On top of this, I haven't found any documentation specifying how fastexcel is even handling duplicated columns.
f"{column}{sep}{i}"The only way I have been able to do it is by basically reading the sheet twice, first the header row, transforming it using custom
make_uniquefunction, and then replacing the names from second read.My custom
make_uniquefunction looks like this:_DUPLICATED_and_UNNAMED_might look ugly, but they have much smaller chance to exist in real data. And these are configurable, so if user needs something else, they can configure it themselves.Consider comparison with R's tidyverse workhorse tibble:
or a similar option, though less convenient, in R base::data.frame
in which case user can toggle
check.names=FALSEand then fix them themselves (which is possible with data.frames since they are just arrays of columns and unique names are not required)