S
Solara6mo ago
Roman

Is it possible to update a df assigned to a reactive object without recreating it ?

i am aiming at something like this : --- df = solara.reactive(pd.DataFrame(...)) @solara.component def Page(): (...) In a children component a function takes df as an arg, computes a new column and return the updated df. The updated df is available for other children components ---
9 Replies
MaartenBreddels
MaartenBreddels6mo ago
Good question, I also had some trouble getting this right, so I put together this example, does that help you : https://py.cafe/maartenbreddels/solara-mutate-dataframe
PyCafe - Solara - Mutate a dataframe, instead of a copy
PyCafe: get your daily fix of Python!
Roman
RomanOP6mo ago
to give more context : no df example : factor = solara.reactive(1) @solara.component def Page(): population = [round( 100 *(1+0.1)**i) for i in range(0, 10)] new = [p * factor.value for p in population] with Column(): InputInt('Factor', value=factor) Markdown(str(new)) Page() but this doesn't : factor = solara.reactive(1) @solara.component def Page(): df_1['new'] = df_1['population'] * factor.value with Column(): InputInt('Factor', value=factor) DataFrame(df_1) Page() by not working i mean that the df doesn't get updated while the list does (access bloked by my company i'll have a look back home but thanks !!)
MaartenBreddels
MaartenBreddels6mo ago
import solara
from great_tables import GT
from great_tables.data import sp500

# we still wrap it in a reactive, so each user can work on its own copy
df = solara.reactive(sp500)
# this is a proxy to detect changes to the dataframe, since we will mutate df.value
df_version = solara.reactive(0)

# however, since the initial value is shared, we do not want to mutate
# it for each user
# NOTE: this may change in the future https://github.com/widgetti/solara/pull/595
def copy_once():
df.value = df.value.copy()
solara.lab.on_kernel_start(copy_once)

# Define the start and end dates for the data range
start_date = "2010-06-07"
end_date = "2010-06-14"

@solara.component
def Page():
df_version.value # just read it to make this component depend on the proxy
print("Render: ", df_version.value)

def multiply():
# we mutate the dataframe, Python/solara cannot detect that
df.value['volume'] = df.value['volume'] * 2
# but we use this proxy to signal we changed it
df_version.value += 1

# Filter sp500 using Pandas to dates between `start_date` and `end_date`
dff = df.value
dff = dff[(dff["date"] >= start_date) & (dff["date"] <= end_date)]

# Create a display table based on the `sp500_mini` table data
with solara.Column(align="center"):
display(
GT(dff)
.tab_header(title="S&P 500", subtitle=f"{start_date} to {end_date}")
.fmt_currency(columns=["open", "high", "low", "close"])
.fmt_date(columns="date", date_style="wd_m_day_year")
.fmt_number(columns="volume", compact=True)
.cols_hide(columns="adj_close")
)
with solara.Row(margin=8):
solara.Button("Multiply", on_click=multiply, outlined=True, color="primary")
import solara
from great_tables import GT
from great_tables.data import sp500

# we still wrap it in a reactive, so each user can work on its own copy
df = solara.reactive(sp500)
# this is a proxy to detect changes to the dataframe, since we will mutate df.value
df_version = solara.reactive(0)

# however, since the initial value is shared, we do not want to mutate
# it for each user
# NOTE: this may change in the future https://github.com/widgetti/solara/pull/595
def copy_once():
df.value = df.value.copy()
solara.lab.on_kernel_start(copy_once)

# Define the start and end dates for the data range
start_date = "2010-06-07"
end_date = "2010-06-14"

@solara.component
def Page():
df_version.value # just read it to make this component depend on the proxy
print("Render: ", df_version.value)

def multiply():
# we mutate the dataframe, Python/solara cannot detect that
df.value['volume'] = df.value['volume'] * 2
# but we use this proxy to signal we changed it
df_version.value += 1

# Filter sp500 using Pandas to dates between `start_date` and `end_date`
dff = df.value
dff = dff[(dff["date"] >= start_date) & (dff["date"] <= end_date)]

# Create a display table based on the `sp500_mini` table data
with solara.Column(align="center"):
display(
GT(dff)
.tab_header(title="S&P 500", subtitle=f"{start_date} to {end_date}")
.fmt_currency(columns=["open", "high", "low", "close"])
.fmt_date(columns="date", date_style="wd_m_day_year")
.fmt_number(columns="volume", compact=True)
.cols_hide(columns="adj_close")
)
with solara.Row(margin=8):
solara.Button("Multiply", on_click=multiply, outlined=True, color="primary")
Run and edit this code snippet at PyCafe
Monty Python
Monty Python6mo ago
maartenbreddels
<:pull_draft:882464249065136138> [widgetti/solara] feat: detect mutation to values of reactive vars
A common source of error is mutation of values in reactive vars. The reactive var cannot notice a change in its value (e.g. a list) if the list is mutated in place. A user can be mislead that it is working correctly, when another reactive variable triggers a rerender, teaching bad habits. Detecting mutations in Python without using proxies can be done by making a deepcopy of the object, and comparing the reference to the deepcopy. This comes at the cost of performance and memory usage, therefore we should enabled this by default in development mode (tests run in this mode by default), so app builders are aware of mistakes while developing. In production mode we can disable the mutation checks and behave as before. TODO: - ◻️ Trigger calls to check_mutations() from several places: after an event handler (requires a change in reacton) and after a component run. - ◻️ We probably do not want two equals function in solara/reacton, reconcile this. - ◻️ Give an option to opt out of the mutation check per reactive var (e.g. when equals fails), or for performance reasons. - ◻️ support reactive.get(copy=True) to always get a copy, even when _CHECK_MUTATIONS is False - ◻️ Do we need support reactive.get(reference=True) to always get a reference, or is the opt-out enough?
Created
Cyrus
Cyrus6mo ago
could you use solara.lab.computed?
Roman
RomanOP6mo ago
@MaartenBreddels tried something a little bit different and it doesn't work : the df doesn't get updated dynamically
from solara import *
import pandas as pd

df = solara.reactive(pd.DataFrame({'year' : [2025 + i for i in range(0, 10)]}))

start_pop = solara.reactive(100)
g_rate = solara.reactive(0.1)
r_rate = solara.reactive(0.95)

df_version = solara.reactive(0)

def copy_once():
df.value = df.value.copy()
solara.lab.on_kernel_start(copy_once)

@solara.component
def Population():

df_version.value

def update_pop():
df.value['population'] = [round(r_rate.value * start_pop.value*( 1+ g_rate.value)**i) for i in range(0, 10)]
df_version.value += 1

dff = df.value

with Columns([1,4]):
with Column():
InputInt('initial population', value=start_pop)
InputFloat('Growth rate', value=g_rate)
InputFloat('Retention rate', value=r_rate)
with Column():
Button('Update', on_click=update_pop)
DataFrame(dff)
from solara import *
import pandas as pd

df = solara.reactive(pd.DataFrame({'year' : [2025 + i for i in range(0, 10)]}))

start_pop = solara.reactive(100)
g_rate = solara.reactive(0.1)
r_rate = solara.reactive(0.95)

df_version = solara.reactive(0)

def copy_once():
df.value = df.value.copy()
solara.lab.on_kernel_start(copy_once)

@solara.component
def Population():

df_version.value

def update_pop():
df.value['population'] = [round(r_rate.value * start_pop.value*( 1+ g_rate.value)**i) for i in range(0, 10)]
df_version.value += 1

dff = df.value

with Columns([1,4]):
with Column():
InputInt('initial population', value=start_pop)
InputFloat('Growth rate', value=g_rate)
InputFloat('Retention rate', value=r_rate)
with Column():
Button('Update', on_click=update_pop)
DataFrame(dff)
MaartenBreddels
MaartenBreddels6mo ago
(PS: please give a working example next time, including the import, ideally by testing it on pycafe, that saves me precious time). The problem here, is that DataFrame also does not see its arguments change (it's the same object). The solution to this is: DataFrame(dff.copy(deep=False)) - which should be performant https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.copy.html Looking back at the original issue, maybe it's better to not use the version proxy, and always make a shallow copy, wouldn't that be simpler?
Roman
RomanOP6mo ago
(my bad for the incomplete example, it tested it in a notebook (in VSCode)) thanks for the solution I think I lack some python litteracy : why would my last example need to use a shallow copy while yours doesn't ?
MaartenBreddels
MaartenBreddels6mo ago
solara.Dataframe (or any component for that matter) checks if the argument changed. Since it is the same object, it concludes it did not change, and will not rerender.
Want results from more Discord servers?
Add your server