Ответ 1
Ах, это потому, что в вашем "неудачном случае" значения df["z"].values
возвращают копию того, что хранится в столбце 'z'
df
. Он не имеет ничего общего с функцией numba:
>>> import pandas as pd
>>> import numpy as np
>>> df = pd.DataFrame([[0, 3, 0, 1, np.nan]], columns=['v', 'y', 'v', 'x', 'z'])
>>> np.shares_memory(df['z'].values, df['z'])
False
В то время как в "рабочем случае" это вид в столбце 'z'
:
>>> df = pd.DataFrame([[0, 3, 1, np.nan]], columns=['v', 'y', 'x', 'z'])
>>> np.shares_memory(df['z'].values, df['z'])
True
NB: На самом деле довольно забавно, что это работает, потому что копия выполняется, когда вы выполняете df['z']
когда вы не .values
доступ к .values
.
Вывод здесь заключается в том, что вы не можете ожидать, что индексирование DataFrame или доступ к .values
серии всегда будут возвращать представление. Поэтому обновление столбца на месте не может изменить значения оригинала. Проблема не только в дублировании имен столбцов. Когда values
свойств возвращают копию и когда она возвращает представление, не всегда ясны (кроме pd.Series
то это всегда представление). Но это только детали реализации. Поэтому никогда не стоит полагаться на конкретное поведение здесь. Единственная гарантия, которую .values
заключается в том, что она возвращает numpy.ndarray
содержащий те же значения.
Однако довольно легко избежать этой проблемы, просто вернув измененный столбец z
из функции:
import numba as nb
import numpy as np
import pandas as pd
@nb.njit
def f_(n, x, y, z):
for i in range(n):
z[i] = x[i] * y[i]
return z # this is new
Затем присвойте результат функции столбцу:
>>> df = pd.DataFrame([[0, 3, 0, 1, np.nan]], columns=['v', 'y', 'v', 'x', 'z'])
>>> df['z'] = f_(df.shape[0], df["x"].values, df["y"].values, df["z"].values)
>>> df
v y v x z
0 0 3 0 1 3.0
>>> df = pd.DataFrame([[0, 3, 1, np.nan]], columns=['v', 'y', 'x', 'z'])
>>> df['z'] = f_(df.shape[0], df["x"].values, df["y"].values, df["z"].values)
>>> df
v y x z
0 0 3 1 3.0
В случае, если вас интересует, что произошло в вашем конкретном случае в настоящее время (как я уже упоминал, мы говорим о деталях реализации здесь, поэтому не принимайте это, как указано. Это именно то, как оно реализовано сейчас). Если у вас есть DataFrame, он будет хранить столбцы с одним и тем же dtype
в многомерном массиве NumPy. Это можно увидеть, если вы обращаетесь к атрибуту blocks
(устарели, потому что внутреннее хранилище может измениться в ближайшем будущем):
>>> df = pd.DataFrame([[0, 3, 0, 1, np.nan]], columns=['v', 'y', 'v', 'x', 'z'])
>>> df.blocks
{'float64':
z
0 NaN
,
'int64':
v y v x
0 0 3 0 1}
Обычно очень просто создать представление в этом блоке, переведя имя столбца в индекс столбца соответствующего блока. Однако, если у вас есть дублирующееся имя столбца, доступ к произвольному столбцу не может быть гарантированным представлением. Например, если вы хотите получить доступ к 'v'
тогда он должен индексировать блок Int64 с индексами 0 и 2:
>>> df = pd.DataFrame([[0, 3, 0, 1, np.nan]], columns=['v', 'y', 'v', 'x', 'z'])
>>> df['v']
v v
0 0 0
Технически можно было бы индексировать не дублированные столбцы как представления (и в этом случае даже для дублированного столбца, например, используя Int64Block[::2]
но это очень частный случай...). Pandas выбирает безопасную опцию всегда возвращать копию, если есть повторяющиеся имена столбцов (имеет смысл, если вы думаете об этом. Зачем индексировать один столбец, возвращать представление, а другой возвращает копию). DataFrame
имеет явную проверку для дубликатов столбцов и рассматривает их по-разному (в результате получается копия):
def _getitem_column(self, key):
""" return the actual column """
# get column
if self.columns.is_unique:
return self._get_item_cache(key)
# duplicate columns & possible reduce dimensionality
result = self._constructor(self._data.get(key))
if result.columns.is_unique:
result = result[key]
return result
columns.is_unique
- важная линия здесь. Это True
для вашего "нормального случая", но "ложно" для "неудачного случая".