Был указан следующий код:
import polars as pl
import numpy as np
# Set the random seed for reproducibility
np.random.seed(0)
# Define the sample size
n = 35000000
# Define the possible values
values_CRP = ["10", "2", "3", None, "<4", ">5"]
values_CRP2 = ["10", "12", "<5", "NA", ">5", "5"]
values_CRP3 = ["10", "12.3", "<5", "NA", ">5.5", "4"]
# Create the DataFrame
df = pl.DataFrame({
"CRP": np.random.choice(values_CRP, n, replace=True),
"CRP2": np.random.choice(values_CRP2, n, replace=True),
"CRP3": np.random.choice(values_CRP3, n, replace=True)
})
Предположим, что это столбцы трех разных биомаркеров. Я хочу очистить их, захватив числовую часть каждого строкового значения. Если строка не начинается с «<» или «>» («10» должно давать 10), оставьте нули как есть (это означает, что пациент не иметь какие-либо измерения в этот момент времени) и замените значения, начинающиеся с «<» или «>», медианой значений, которые находятся ниже или выше второго элемента соответствующего значения. Например, «<5» следует заменить медианой наблюдений со значением биомаркера ниже 5. За «>5» берем наблюдения выше. Если у нас есть значение «>10000» и нет наблюдений выше 10000, то мы обнуляем его. То же самое с <.
Желаемый результат для минимального примера:
df1 = pl.DataFrame({"Current":["<4","3", "2", None, ">5", "10"],
"Goal": [2.5,3,2,None,10,10]})
В идеале, поскольку в реальности у меня 11 столбцов и почти 40 миллионов строк, мне бы хотелось делать как можно больше в ленивом режиме.
Я думаю, вам нужно будет соединить dataframe с самим собой или запустить какой-то подзапрос, и для этих задач я считаю, что DuckDB очень удобен для пользователя:
df = df.with_columns(
num = pl.col.Current.cast(pl.Float64, strict=False)
)
duckdb.sql("""
select
d.Current,
d.Goal,
case
when d.Current[1] == '<' then
(select median(tt.num) from df as tt where tt.num < try_cast(d.Current[2:] as float))
when d.Current[1] == '>' then
(select median(tt.num) from df as tt where tt.num > try_cast(d.Current[2:] as float))
else d.num
end as Calc
from df as d
""").pl()
shape: (6, 3)
┌─────────┬──────┬──────┐
│ Current ┆ Goal ┆ Calc │
│ --- ┆ --- ┆ --- │
│ str ┆ f64 ┆ f64 │
╞═════════╪══════╪══════╡
│ <4 ┆ 2.5 ┆ 2.5 │
│ 3 ┆ 3.0 ┆ 3.0 │
│ 2 ┆ 2.0 ┆ 2.0 │
│ null ┆ null ┆ null │
│ >5 ┆ 10.0 ┆ 10.0 │
│ 10 ┆ 10.0 ┆ 10.0 │
└─────────┴──────┴──────┘
Для чистых полярных значений вы, вероятно, могли бы сначала предварительно рассчитать медиану для всех уникальных значений, а затем просто объединить:
df_num = df.select(num = pl.col.Current.cast(pl.Float64, strict=False)).drop_nulls()
df_calc = (
df
.filter(pl.col.Current.str.head(1).is_in(["<",">"]))
.select(
pl.col.Current,
oper = pl.col.Current.str.head(1),
bound = pl.col.Current.str.tail(-1).cast(pl.Float64)
)
.unique()
)
df_mapping = (
df_num
.join(df_calc, how = "cross")
.filter(
((pl.col.oper == ">") & (pl.col.num > pl.col.bound)) |
((pl.col.oper == "<") & (pl.col.num < pl.col.bound))
)
.group_by("Current")
.agg(pl.col.num.median())
)
(
df
.join(df_mapping, on = "Current", how = "left")
.with_columns(pl.col.num.fill_null(pl.col.Current))
)
ape: (6, 3)
┌─────────┬──────┬──────┐
│ Current ┆ Goal ┆ num │
│ --- ┆ --- ┆ --- │
│ str ┆ f64 ┆ f64 │
╞═════════╪══════╪══════╡
│ <4 ┆ 2.5 ┆ 2.5 │
│ 3 ┆ 3.0 ┆ 3.0 │
│ 2 ┆ 2.0 ┆ 2.0 │
│ null ┆ null ┆ null │
│ >5 ┆ 10.0 ┆ 10.0 │
│ 10 ┆ 10.0 ┆ 10.0 │
└─────────┴──────┴──────┘
Если вы хотите расширить его на несколько столбцов, вы, вероятно, можете сделать то же самое, но unpivot() сначала ваш DataFrame, вычислите значения, а затем Pivot() все обратно.
df = pl.DataFrame({
"CRP1":["<4","3", "2", None, ">5", "10"],
"CRP2":["1","2", None, "3", "4", "<2"]
})
shape: (6, 2)
┌──────┬──────┐
│ CRP1 ┆ CRP2 │
│ --- ┆ --- │
│ str ┆ str │
╞══════╪══════╡
│ <4 ┆ 1 │
│ 3 ┆ 2 │
│ 2 ┆ null │
│ null ┆ 3 │
│ >5 ┆ 4 │
│ 10 ┆ <2 │
└──────┴──────┘
df_unpivot = df.with_row_index().unpivot(index = "index")
df_num = (
df_unpivot
.select(
pl.col.variable,
num = pl.col.value.cast(pl.Float64, strict=False)
)
.drop_nulls()
)
df_calc = (
df_unpivot
.filter(pl.col.value.str.head(1).is_in(["<",">"]))
.select(
pl.col.variable,
pl.col.value,
oper = pl.col.value.str.head(1),
bound = pl.col.value.str.tail(-1).cast(pl.Float64)
)
.unique()
)
df_mapping = (
df_calc
.join(df_num, on = "variable", how = "inner")
.filter(
((pl.col.oper == ">") & (pl.col.num > pl.col.bound)) |
((pl.col.oper == "<") & (pl.col.num < pl.col.bound))
)
.group_by("variable","value")
.agg(pl.col.num.median())
)
(
df_unpivot
.join(df_mapping, on=["variable","value"], how = "left")
.with_columns(pl.col.num.fill_null(pl.col.value).cast(pl.Float64))
.pivot("variable", index = "index", values=["value","num"])
)
shape: (6, 5)
┌───────┬────────────┬────────────┬──────────┬──────────┐
│ index ┆ value_CRP1 ┆ value_CRP2 ┆ num_CRP1 ┆ num_CRP2 │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ u32 ┆ str ┆ str ┆ f64 ┆ f64 │
╞═══════╪════════════╪════════════╪══════════╪══════════╡
│ 0 ┆ <4 ┆ 1 ┆ 2.5 ┆ 1.0 │
│ 1 ┆ 3 ┆ 2 ┆ 3.0 ┆ 2.0 │
│ 2 ┆ 2 ┆ null ┆ 2.0 ┆ null │
│ 3 ┆ null ┆ 3 ┆ null ┆ 3.0 │
│ 4 ┆ >5 ┆ 4 ┆ 10.0 ┆ 4.0 │
│ 5 ┆ 10 ┆ <2 ┆ 10.0 ┆ 1.0 │
└───────┴────────────┴────────────┴──────────┴──────────┘
Я не использую столбец «Цель», я оставил его здесь только для сравнения.
Извиняюсь, меня смутил просто fill_null в конце. Я собираюсь попробовать это в ближайшее время на своих данных.
ах да, моя вина, это должно быть Current
конечно, исправлено
Есть идеи, как применить ваш процесс программирования ко всем переменным, которые требуют этого процесса?
Можно запустить его с переменными столбцами, но он, вероятно, будет включать unpivot, который еще больше увеличит размер вашего набора данных, если у вас 11 столбцов, может быть, вы можете просто запустить цикл по этим столбцам, а затем объединить результаты в новый DataFrame?
Можете ли вы показать свою попытку? Я не знаком с Python и в основном использую R.
Давайте продолжим обсуждение в чате.
Вы используете информацию из pl.col.Goal, чего у нас вообще не будет. Как я уже писал, исходный фрейм данных указан в начале моего вопроса.