# Data input

Whittaker accepts data as a Python dictionary mapping column names to 1-D NumPy arrays. This is the simplest and most portable data format. It works with any data source and avoids coupling the library to a specific DataFrame backend.


# The data dictionary

Every `fit()` and `predict()` call expects a dictionary where keys are column names (strings) and values are 1-D NumPy arrays of the same length.


``` python
import numpy as np
import whittaker as wk

# The data dictionary: one key per column, one array per key
data = {
    "x1": np.linspace(0, 2 * np.pi, 100),
    "x2": np.random.default_rng(23).uniform(0, 1, 100),
    "y": np.sin(np.linspace(0, 2 * np.pi, 100))
    + np.random.default_rng(23).normal(0, 0.3, 100),
}

# Fit a model (the formula references keys from the dictionary)
model = wk.GAM("y ~ s(x1) + s(x2)")
model.fit(data, method="REML")

print(f"Fitted with {len(data['y'])} observations")
```


    Fitted with 100 observations


Every column referenced in the formula must be present in the dictionary. Extra columns are ignored.


# Converting from DataFrames

If your data is in a Polars or Pandas DataFrame, convert it to a dictionary before passing it to Whittaker.


## From Polars


``` python
import polars as pl

# Create a Polars DataFrame
df_pl = pl.DataFrame(
    {
        "x": np.linspace(0, 2 * np.pi, 100),
        "y": np.sin(np.linspace(0, 2 * np.pi, 100))
        + np.random.default_rng(23).normal(0, 0.3, 100),
    }
)

# Convert to a dict of NumPy arrays
data_from_polars = {col: df_pl[col].to_numpy() for col in df_pl.columns}

# Fit the model
model = wk.GAM("y ~ s(x)")
model.fit(data_from_polars, method="REML")
print(f"Fitted from Polars: EDF = {model.edf_total:.1f}")
```


    Fitted from Polars: EDF = 7.5


## From Pandas


``` python
import pandas as pd

# Create a Pandas DataFrame
df_pd = pd.DataFrame(
    {
        "x": np.linspace(0, 2 * np.pi, 100),
        "y": np.sin(np.linspace(0, 2 * np.pi, 100))
        + np.random.default_rng(23).normal(0, 0.3, 100),
    }
)

# Convert to a dict of NumPy arrays
data_from_pandas = {col: df_pd[col].to_numpy() for col in df_pd.columns}

# Fit the model
model = wk.GAM("y ~ s(x)")
model.fit(data_from_pandas, method="REML")
print(f"Fitted from Pandas: EDF = {model.edf_total:.1f}")
```


    Fitted from Pandas: EDF = 7.5


> **Tip: Zero-copy conversion**
>
> For Polars DataFrames backed by Arrow arrays, `to_numpy()` is often zero-copy, so no data is duplicated. For Pandas with NumPy-backed columns, `to_numpy()` returns a view of the underlying array. This means conversion is essentially free for typical numerical data.


# Prediction data

The `predict()` method accepts the same dictionary format. You only need to include the covariate columns (the response column is not required for prediction).


``` python
# Predict on new data (only covariate columns are needed)
new_data = {"x": np.linspace(0, 2 * np.pi, 50)}

preds = model.predict(new_data)
print(f"Predictions shape: {preds.values.shape}")
```


    Predictions shape: (50,)


``` python
import altair as alt

x_plot = np.linspace(0, 2 * np.pi, 100)
pred_plot = model.predict({"x": x_plot}, se=True)

plot_data = [
    {"x": float(x_plot[i]), "fit": float(pred_plot.values[i]),
     "lower": float(pred_plot.values[i] - 1.96 * pred_plot.se[i]),
     "upper": float(pred_plot.values[i] + 1.96 * pred_plot.se[i])}
    for i in range(len(x_plot))
]

pts = alt.Chart(
    {"values": [{"x": float(data_from_pandas["x"][i]), "y": float(data_from_pandas["y"][i])}
     for i in range(len(data_from_pandas["y"]))]}
).mark_circle(size=15, opacity=0.3, color="steelblue").encode(
    x=alt.X("x:Q", title="x"), y=alt.Y("y:Q", title="y"),
)

band = alt.Chart({"values": plot_data}).mark_area(
    opacity=0.2, color="firebrick"
).encode(x="x:Q", y="lower:Q", y2="upper:Q")

line = alt.Chart({"values": plot_data}).mark_line(
    color="firebrick", strokeWidth=2
).encode(x="x:Q", y="fit:Q")

(band + pts + line).properties(
    width="container", height=300, title="Predictions from the fitted GAM"
)
```


<style>
  #altair-viz-13abe04d043d4d67a67dd6fca112ab8e.vega-embed {
    width: 100%;
    display: flex;
  }

  #altair-viz-13abe04d043d4d67a67dd6fca112ab8e.vega-embed details,
  #altair-viz-13abe04d043d4d67a67dd6fca112ab8e.vega-embed details summary {
    position: relative;
  }
</style>


# Missing values

GAMs do not support missing values in model covariates. If your data contains `NaN` values, Whittaker will raise an error at fit time. Remove or impute missing values before fitting.


``` python
# Example: cleaning missing values before fitting
rng = np.random.default_rng(23)
x_with_nans = np.array([1.0, 2.0, np.nan, 4.0, 5.0, np.nan, 7.0, 8.0, 9.0, 10.0])
y_with_nans = np.sin(x_with_nans)

# Drop rows with NaN in any column
mask = np.isfinite(x_with_nans) & np.isfinite(y_with_nans)
clean_data = {
    "x": x_with_nans[mask],
    "y": y_with_nans[mask],
}

print(f"Original: {len(x_with_nans)} rows")
print(f"After cleaning: {len(clean_data['x'])} rows")
```


    Original: 10 rows
    After cleaning: 8 rows


# Data types

All values are converted to `float64` internally. Integer arrays, boolean arrays, and other numeric types are converted automatically. Non-numeric data (strings, objects) cannot be used directly. Encode categorical variables as numeric indicators before passing them to Whittaker.


``` python
# Integer data is converted to float automatically
data_int = {
    "x": np.arange(50),
    "y": np.random.default_rng(23).poisson(3, 50).astype(float),
}

model = wk.GAM("y ~ s(x)", family=wk.Poisson())
model.fit(data_int, method="REML")
print(f"Fitted with integer covariates: EDF = {model.edf_total:.1f}")
```


    Fitted with integer covariates: EDF = 2.0


The integer array is converted internally and the model fits as expected.


# Functional covariates

For [functional regression](functional.md), functional covariates are passed as 2-D arrays in the data dictionary. Each row is one observation's curve, and each column is one grid point along the functional domain.


``` python
# Functional covariate: each row is a curve observed at 50 grid points
rng = np.random.default_rng(23)
n, T = 100, 50
X_func = rng.normal(0, 1, (n, T)).cumsum(axis=1) / np.sqrt(T)

# The data dict can mix 1-D (scalar) and 2-D (functional) arrays
data_func = {
    "curves": X_func,  # shape (100, 50), functional covariate
    "y": rng.normal(0, 1, n),  # shape (100,), scalar response
}

print(f"Functional covariate shape: {data_func['curves'].shape}")
print(f"Response shape: {data_func['y'].shape}")
```


    Functional covariate shape: (100, 50)
    Response shape: (100,)


The 2-D array stores one curve per row. See the [functional regression page](functional.md) for the full workflow of fitting and interpreting functional GAMs.


# Where to go next

- **[Quick start](quick-start.md)**: fit a complete GAM from a data dictionary in a few lines.
- **[Smooth terms](smooths.md)**: the smooth types available and how to configure them.
- **[Functional regression](functional.md)**: fitting models with functional covariates.
- **[Large datasets](large-datasets.md)**: scalable backends when the data does not fit in memory.
