Несоответствующее поведение джиттовой функции

У меня очень простая функция:

import numpy as np
from numba import jit
import pandas as pd

@jit
def f_(n, x, y, z):
    for i in range(n):
        z[i] = x[i] * y[i] 

f_(df.shape[0], df["x"].values, df["y"].values, df["z"].values)

К которому я прохожу

df = pd.DataFrame({"x": [1, 2, 3], "y": [3, 4, 5], "z": np.NaN})

Я ожидал, что функция изменит столбец данных z на месте следующим образом:

>>> f_(df.shape[0], df["x"].values, df["y"].values, df["z"].values)
>>> df

   x  y     z
0  1  3   3.0
1  2  4   8.0
2  3  5  15.0

Это работает отлично в большинстве случаев, но каким-то образом не удается изменить данные в других.

Я дважды проверял вещи и:

  • Я не определил никаких проблем с точками данных, которые могут вызвать эту проблему.
  • Я вижу, что данные меняются, как ожидается, когда я печатаю результат.
  • Если я z массив z из функции, он будет изменен, как и ожидалось.

К сожалению, я не мог свести проблему к минимальному воспроизводимому случаю. Например, удаление несвязанных столбцов, по-видимому, "исправляет" проблему, делая невозможным сокращение.

Я использую jit таким образом, который не предназначен для использования? Есть ли какие-либо пограничные случаи, о которых я должен знать? Или это может быть ошибка?

Изменить:

Я нашел источник проблемы. Это происходит, когда данные содержат дублированные имена столбцов:

>>> df_ = pd.read_json('{"schema": {"fields":[{"name":"index","type":"integer"},{"name":"v","type":"integer"},{"name":"y","type":"integer"},
... {"name":"v","type":"integer"},{"name":"x","type":"integer"},{"name":"z","type":"number"}],"primaryKey":["index"],"pandas_version":"0.20.
... 0"}, "data": [{"index":0,"v":0,"y":3,"v":0,"x":1,"z":null}]}', orient="table")
>>> f_(df_.shape[0], df_["x"].values, df_["y"].values, df_["z"].values)
>>> df_
   v  y  v  x   z
0  0  3  0  1 NaN

Если дубликат удален, функция работает так, как ожидалось:

>>> df_.drop("v", axis="columns", inplace=True)
>>> f_(df_.shape[0], df_["x"].values, df_["y"].values, df_["z"].values)
>>> df_
   y  x    z
0  3  1  3.0

Ответы

Ответ 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 для вашего "нормального случая", но "ложно" для "неудачного случая".