<jupyter_start><jupyter_text>Table of ContentsBeer Cooling PredictionThe GoalIssues to considerThis NotebookPart 1: Complete the missing codePart 2: Main SectionYour TaskPart 1: Complete the code1a. Complete prepare_cooling_data1b. Clean data1c. Extract all rows related to cooling phase, compute cooling elapsed time1d. Define temperature equationNon-linear regression: computation and predictionNOTE: there is no code to complete for those 2 functionsPart 2: Main Section2a. Set input parameters2b. Initialize Hub client2c. Download data from OCS with Data ViewsFind predefined data view for Fermenter 32 and "Cooling"Data View StructureGet Interpolated Data from Data View2d. Main AnalysisAnalysis parametersPlot data from data view result for brand of interest, keep only important brewing phaseCompute cooling elapsed timeRearrange dataframe and plot batches dataRegression parameters2e. Plot prediction curve along with actual data --- Beer Cooling Prediction---This learning module, Beer Cooling Prediction, focuses on the cooling phase wherein the temperature of the solution drops from 70°F to 30°F.  The data was compiled based on the temperature measurements at the top, the middle, and the bottom of the fermenter:![Beer Cooling](https://academichub.blob.core.windows.net/images/NB2_Problem_Statement.png)Attaining a consistent, repeatable cooling profile is critical to the brewing process because the cooling behavior affects the flavor profile of the beers; furthermore, non-ideal cooling behavior can lead to significant process delays. In fact, Deschutes Brewery previously encountered non-ideal cooling behaviors where the temperature spiked when the yeast was harvested. By visualizing the data, Deschutes Brewery was able to identify the source of the issue, correct it, and save 60% of operation time compared to the worst non-ideal cases.  If interested, a more detailed story about the root cause and their corrective action is found in the presentation: [What’s Really Going on with your Beer's Fermentation?](https://www.osisoft.com/Presentations/What-s-Really-Going-on-with-your-Beer-s-Fermentation-/)In this notebook, we will build a predictive model for the cooling temperature profile to predict when the cooling stage should end for some given brand of beer. First, make sure the latest library version for OCS is installed:<jupyter_code>try:
    import ocs_academic_hub
except ModuleNotFoundError:
    !pip install ocs-academic-hub==1.0.1
    import ocs_academic_hub<jupyter_output><empty_output><jupyter_text>Then make sure to import all the required libraries:<jupyter_code># For interaction with OCS
from ocs_academic_hub.datahub import hub_login, timer

import configparser
import datetime as dt
from dateutil import parser
from enum import Enum

try:
    import plotly.express as px 
except ImportError:
    !pip install plotly
    import plotly.express as px 
import numpy
import pandas as pd
from scipy.optimize import curve_fit

pd.set_option("display.expand_frame_repr", False)
pd.options.mode.chained_assignment = None

print("Required libraries and packages imported.")<jupyter_output><empty_output><jupyter_text>--- The GoalUsing the same dataset as in the ADF Prediction notebook, we want to identify the cooling stages and compute the elapsed times to align the data for non-linear regression and comparison.All possible beer brands are for Fermenter Vessel 32 in the time period specified below: * Mirror Pond* Hop Slice* Pacific Wonderland* Black Butte           * Inversion         We'll start with the following input parameters: * Brand: Mirror Pond * Temperature sensors: Bottom, Middle and Top* Training data: 182 days starting at 2017-01-10T00:00 - pick first 2 batches * Interval: 10 minutes (00:10:00) Issues to consider* Real process data is dirty (sensors can fail)* The stages may sometimes have been mislabelled (some examples are "Cooling" sometimes mislabelled as "Diacetyl Rest" or "Maturation" sometimes mislabelled as "Cooling")* There are different cooling start temperatures for each batch and even for each zone* There can be various volumes of beer in the fermentors* Data can have abrupt, aphysical jumps in temperature --- This Notebook Part 1: Complete the missing code Complete TODO sections in the following cells to compute the cooling profile for a vessel zone. The defined functions perform the following tasks    * Clean the data by keeping relevant ([1a](1a.-Complete-prepare_cooling_data)) and removing bad ([1b](1b.-Clean-data)) entries* Identify the cooling stages ([1c](1c.-Extract-all-rows-related-to-cooling-phase,-compute-cooling-elapsed-time)) and compute the elapsed times since the start of cooling * Fit the cooling data to a heat transfer equation ([1d](1d.-Define-temperature-equation))  Part 2: Main SectionThis section actually calls the functions created in Part 1 and plots the prediction curves against each batch for a given brand of beer.* Define parameters of interest, like the time granularity for data interpolation, plus the time period over which to do the analysis and the fermenter identifier to analyze data from ([2a](2a.-Set-input-parameters))* Use OSIsoft Cloud Services (OCS) to obtain process data from Deschutes ([2b](2b.-Initialize-Hub/OCS-client) and [2c](2c.-Download-data-from-OCS-with-Data-Views))* Set brand of interest, which sensor zones to use and batches for regression. Call functions created in Part 1 in sequence with intermediate plots to follow progression.  ([2d](2d.-Main-Analysis))* Plot the cooling prediction curves ([2e](2e.-Plot-prediction-curve-along-with-actual-data))**Section 2d and 2e can be repeated iteratively with different kind of beer, temperature sensor zones and batches to check the impact on the prediction curves.** --- Your Task A few functions in Part 1 contain `TODO` sections in comments. Complete each of them to get a working notebook. If your code is correct, you should see the following graph appear at the bottom of this notebook (at the end of Part 2):![Beer Cooling Prediction](https://academichub.blob.core.windows.net/images/nb2_solution_graph_v4.png) --- Part 1: Complete the code---We want to complete the missing code sections which implement the following specifications:**Input parameters:*** Brand of beer* Which set of temperature sensor to use: bottom, middle, top* Training data: how many days (starting at 2016-02-07) to consider and set of batch identifier(s) for cooling curve regression**Output:** * Plot with all individual batch cooling profiles against their associated prediction curve ------------------------------------------------------------------------------------  1a. Complete `prepare_cooling_data`This is the function that will be executed to:* [1] keep only data for brand of interest* [2] remove rows with missing data, also keep only important phases* [3] indentify and keep only cooling phase rows, compute cooling time in days for each row (allows to compare batches and necessary for regression)<jupyter_code># Phases important for cooling
IMPORTANT_BREWING_PHASES = ["Fermentation", "Free Rise", "Diacetyl Rest", "Cooling"]

# Sensor positions - for indexing in loops
class Position(Enum):
    Bottom = 1
    Middle = 2
    Top = 3

    def __repr__(self):
        return str(self).replace("Position.", "")


def prepare_cooling_data(all_brands_df, brand):
    """
    Input parameters:
    * brand to consider
    * temperature sensor position to use for computation
    * number of days to compute prediction parameters
    """

    # TODO [1]: write filter expression for all_brands_df, return only rows for `brand` of interest
    # =========== STUDENT BEGIN ==========
    brand_df = all_brands_df[@@@ Your code here @@@]
    # =========== STUDENT END ==========

    # TODO [2]: complete code block within function brand_df_cleanup
    brand_status_df = brand_df_cleanup(brand_df)

    # TODO [3]: complete code of function cooling_data_extraction
    cool_df = cooling_data_extraction(brand_status_df)

    # Verify that it was possible to extract the data for a complete cooling phase
    if len(cool_df) == 0:
        raise Exception(f"Error! no cooling data for brand: {brand}")
    print(f"len(cool_df)={len(cool_df)}")

    return cool_df<jupyter_output><empty_output><jupyter_text>1b. Clean data The function `brand_df_cleanup` removes all rows with invalid (missing data) entries for any of the attributes of interest. To predict the cooling profile for a given brand, one must need the following attributes: Brand, Status, Temperature (Top, Middle, and Bottom zones), and Volume.Complete the `TODO` section in the function `brand_df_cleanup`.<jupyter_code>@timer
def brand_df_cleanup(brand_df):

    # TODO [2]: Remove all rows with missing data (which indicates a sensor collection problem)
    # TIP: Check Notebook #1 ADF Prediction (remember how it ended with dropna())
    # =========== STUDENT BEGIN ==========
    brand_df = brand_df.@@@ Your code here @@@
    # =========== STUDENT END ==========

    # Keep only fermentation or post-fermentation stages
    brand_status_df = brand_df[brand_df["Status"].isin(IMPORTANT_BREWING_PHASES)]

    return brand_status_df<jupyter_output><empty_output><jupyter_text>1c. Extract all rows related to cooling phase, compute cooling elapsed timeThe function for extracting all frames in the cooling phase is `cooling_data_extraction`. Overall, `cooling_data_extraction` will extract the temperature information for the cooling stage in a batch in a vessel zone in a fermentor and compute a new column COOLING_TIME with the elapsed time in days since the beginning of cooling.<jupyter_code>@timer
def cooling_data_extraction(brand_status_df):
    #
    # condition for it to be in cooling phase
    # TODO [3]: the condition is that 'Top TIC OUT', 'Middle TIC OUT' and 'Bottom TIC OUT' are above 99.99
    # =========== STUDENT BEGIN ==========
    cool_stage = brand_status_df[
        (brand_status_df["Top TIC OUT"] > 99.99)
        & (@@@ Your code here  @@@)
        & (@@@ Your code here @@@)
    ]
    # =========== STUDENT END ==========

    # TODO [4]: extract first row of each fermentation batch
    # HINT: https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.groupby.html
    # =========== STUDENT BEGIN ==========
    start_time_df = cool_stage_df.groupby(@@@ Your code here @@@).first()
    # =========== STUDENT END ==========

    # Dataframe to hold cooling data with new COOLING_TIME column holding cooling time in days
    cool_df = pd.DataFrame()
    #
    # For each fermentation batch, compute COOLING_TIME
    for ferm_id in list(start_time_df.index):
        start_time = start_time_df.loc[ferm_id].Timestamp
        print(f"  ==> ferm_id={ferm_id} start={start_time}")
        #
        # Dataframe with rows associated to the current fermentation batch ID
        batch_df = cool_stage_df[cool_stage_df["Fermentation ID"] == ferm_id]
        #
        # Cooling time in unit of days
        batch_df.loc[:, COOLING_TIME] = batch_df["Timestamp"].apply(
            lambda t: (t - start_time) / numpy.timedelta64(1, "D")
        )
        #
        # Collect batch_df into result dataframe
        cool_df = cool_df.append(batch_df)

    return cool_df<jupyter_output><empty_output><jupyter_text>**Auxiliary function for plotting** Takes a dataframe with {Top|Middle|Bottom} TIC PV columns and returns a dataframe with a single "Temperature" column instead and a new "Position" column with one of the 3 string {Top|Middle|Bottom} to indicate which sensor the temperature value is associated with.<jupyter_code>def arrange_by_position(cool_df):
    position_dfs = []
    for pos in ["Top", "Middle", "Bottom"]:
        position_df = cool_df[
            ["Brand", "Timestamp", "Fermentation ID", f"{pos} TIC PV", "Volume"]
        ].rename(columns={f"{pos} TIC PV": "Temperature", "Fermentation ID": "Ferm_ID"})
        position_df.loc[:, "Position"] = pos
        if COOLING_TIME in cool_df.columns:
            position_df.loc[:, COOLING_TIME] = cool_df[COOLING_TIME]
        position_dfs += [position_df]

    cooling_df = pd.concat(position_dfs)
    return cooling_df<jupyter_output><empty_output><jupyter_text>1d. Define temperature equation![Cooling equation](https://academicpi.blob.core.windows.net/images/NB2_Heat_transfer_eqn.png)The cell below implements this equation for predicting the cooling curve. The [scipy.curve_fit (click for reference)](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html) function is used find the values for the parameters $\alpha$ and $\beta$ (`alpha` and `beta`) using non-linear least square fit.  **IMPORTANT: To implement the function for $T_{t+1}$ (variable `T_t1`) in the exercise below, you'll need (click link for reference documentation):*** **[numpy.multiply](https://docs.scipy.org/doc/numpy/reference/generated/numpy.multiply.html?highlight=multiplynumpy.multiply)*** **[numpy.reciprocal](https://docs.scipy.org/doc/numpy/reference/generated/numpy.reciprocal.html?highlight=reciprocalnumpy.reciprocal)**<jupyter_code>def temperature_profile(x, alpha, beta):
    # Unpack x values
    T_t = x[0]  # this is a numpy vector
    Vol = x[1]  # another numpy vector

    # TODO [5]: Implement the function defined in red above
    # TIP: check "IMPORTANT" note above, your code should contain variables alpha, beta, T_t and Vol
    # Hint: SOMEWORD and OTHERWORD are two placeholders for where you will put your code 
    # =========== STUDENT BEGIN ==========
    T_t1 = numpy.SOMEWORD(1 + numpy.SOMEWORD(alpha, numpy.OTHERWORD(Vol)), T_t) - (alpha * beta *numpy.OTHERWORD(Vol))
    # =========== STUDENT END ==========
    return T_t1<jupyter_output><empty_output><jupyter_text>Non-linear regression: computation and predictionFunction `compute_cooling_parameters` computes parameter $\alpha$ (`alpha`) and $\beta$ (`beta`) for a given set of fermentation (batch) IDs and temperature zones. Function `generate_prediction_df` input are the above regression parameters and a batch dataframe. It returns a new dataframe with the batch data received and additional prediction rows. The "Position" column value for the new rows is "Prediction". NOTE: there is no code to complete for those 2 functions<jupyter_code>############### CURVE FIT REGRESSION BEGIN - DO NOT CHANGE #############


def compute_cooling_parameters(cool_df, brand, temp_sensors, ferm_ids):
    """
    Input parameters:
    * brand to consider
    * temperature sensor position to use for computation
    * number of days to compute prediction parameters
    """

    # vessel zone(s) where cooling profile will be predicted
    sensors_to_use = [repr(pos) for pos in Position if temp_sensors[repr(pos)]]
    # at least one should be true
    if len(sensors_to_use) == 0:
        raise Exception(
            "@@@ Error: at least one of Bottom|Middle|Top should be true in TEMP_SENSORS"
        )
    print(f"sensors to use={sensors_to_use} with batch(es) {ferm_ids}")

    # Only the fermentation IDs and specified sensor zones are kept for regression
    regression_cool_df = cool_df[
        cool_df["Ferm_ID"].isin(ferm_ids) & cool_df["Position"].isin(sensors_to_use)
    ]

    # sort the temperatures in a descending fashion
    regression_cool_df = regression_cool_df.sort_values(
        by=["Temperature"], ascending=False
    )

    # get the y value for the x, this will be used in curve fitting
    regression_cool_df["Temperature_y"] = regression_cool_df["Temperature"].shift(-1)

    # drop the last row
    regression_cool_df = regression_cool_df[:-1]

    # training temperature feature
    x1_train = regression_cool_df["Temperature"].values

    # training Volume feature
    x2_train = regression_cool_df["Volume"].values

    # [temperature, volume]
    x = [x1_train, x2_train]

    # Training of non-linear least squares model
    # Nonlinear curve-fitting pass a tuple in curve fitting
    popt, pcov = curve_fit(
        temperature_profile, x, regression_cool_df["Temperature_y"].values
    )

    # get the coefficients alpha and beta in the model
    alpha = popt[0]
    beta = popt[1]

    # compute mean of maximum elapsed time for all (batch ID, position) pair
    regression_cooling_elapsed = (
        regression_cool_df.groupby(by=["Ferm_ID", "Position"]).max()
    )[COOLING_TIME].mean()
    return (alpha, beta, regression_cooling_elapsed)


############### CURVE FIT REGRESSION END - DO NOT CHANGE #############
def generate_prediction_df(
    alpha,
    beta,
    brand,
    pos_cool_df,
    temp_sensors,
    ferm_id,
    max_cooling_days,
    reg_batch_count=1,
):
    # vessel zone(s) where cooling profile will be predicted
    sensors_to_use = [repr(pos) for pos in Position if temp_sensors[repr(pos)]]

    cool_fermid_df = pos_cool_df[pos_cool_df["Ferm_ID"] == ferm_id]

    # Get the initial point of all temperature curves
    T_0s = (
        cool_fermid_df[cool_fermid_df["Position"].isin(sensors_to_use)]
        .groupby(by=["Position"])
        .first()
    )
    T_0 = T_0s["Temperature"].mean()

    # Volumes - NOTE: all 3 sensors position share the same Volume, so we pick the Top one
    Vol = numpy.repeat(
        cool_fermid_df[cool_fermid_df["Position"].isin(sensors_to_use)][
            "Volume"
        ].values,
        reg_batch_count,
    )

    # Compute the prediction with the start temperature T_0
    T_prediction = [T_0]
    T_t = T_0
    for i in range(1, len(Vol)):
        x = [numpy.atleast_1d(T_t), numpy.atleast_1d(Vol[i])]
        # result is T_t1 computed from T_t
        T_t = temperature_profile(x, alpha, beta)[0]
        T_prediction.append(T_t)

    prediction_df = pd.DataFrame(T_prediction, columns=["Temperature"])
    prediction_df.loc[:, "Brand"] = brand
    prediction_df.loc[:, "Ferm_ID"] = ferm_id
    prediction_df.loc[:, "Position"] = "Prediction"
    prediction_df.loc[:, COOLING_TIME] = numpy.linspace(0.0, max_cooling_days, len(Vol))

    cool_fermid_df = cool_fermid_df.append(prediction_df, sort=True)

    return (T_0, prediction_df, cool_fermid_df)<jupyter_output><empty_output><jupyter_text>--- Part 2: Main Section---Once all functions above are fully implemented and OCS has been initialized, below are the cells to:[2a](section_2a) - Set the input parameters   [2b](section_2b) - Initialize OCS client[2c](section_2c) - Download data from OCS with Data View[2d](section_2d) - Call functions of Part 1 to build result dataframe. This is the main analysis.   [2e](section_2e) - Plot result data  2a. Set input parametersSpecify what fermentor, time period and interpolation interval we want for the data view.<jupyter_code># time period over which to analyze data
TRAINING_DAYS = 182

# we want to pull this data starting from timestamp "start_index"
START_INDEX = "2017-01-20T15:30"
END_INDEX = (parser.parse(START_INDEX) + dt.timedelta(days=TRAINING_DAYS)).isoformat()

# time granularity
INTERVAL = "00:10:00"

# specify asset 
ASSET_ID = "FV32"

# Additional column name for calculated cooling time in indicated unit
COOLING_TIME = "Cooling (days)"<jupyter_output><empty_output><jupyter_text>2b. Initialize Hub client**Execute the cell below and follow the indicated steps to log in (an AVEVA banner would show up)**<jupyter_code>widget, hub_client = hub_login()
widget
dataset = hub_client.current_dataset()
namespace_id = hub_client.namespace_of(dataset)
print(f"Current dataset is {dataset} in namespace_id: {namespace_id}")<jupyter_output><empty_output><jupyter_text>2c. Download data from OCS with Data Views**Development tip (WARNING: getting data view results can take up to 30 secs)** Development of a notebook involves running code over and over, so you'll want to avoid long running steps when possible. This is why you can run the cell below once, with the resulting dataframe saved in variable `all_brands_df`. If you don't change any of its input parameters, `all_brands_df` is still valid and can be reused when doing your analysis with different parameters. Also include a statistical description of `all_brands_df`.  Find predefined data view for Fermenter 32 and "Cooling"<jupyter_code># Filtered to get the one related to "cooling"
# A list with a single element will be returned, so this element is extracted with `[0]`
dataview_id = hub_client.asset_dataviews(filter="cooling", asset=ASSET_ID)[0]
dataview_id<jupyter_output><empty_output><jupyter_text>Data View Structure<jupyter_code>hub_client.dataview_definition(namespace_id, dataview_id)<jupyter_output><empty_output><jupyter_text>Get Interpolated Data from Data View<jupyter_code># pull data from OCS

# TODO: request a Panda dataframe from OCS
# =========== STUDENT BEGIN ==========
all_brands_df = @@@ Your code here @@@.@@@ Your code here @@@(...,...,...,..., INTERVAL)
# =========== STUDENT END ==========

# statistical description of `all_brands_df`, -n/a- stands for "not applicable"
all_brands_df.describe(include="all").fillna('-n/a-')
# preview of dataframe"
all_brands_df.head()
# list brand of beer for cooling with how many rows they appear in
all_brands_df[all_brands_df["Status"] == "Cooling"]["Brand"].value_counts()
# just like in NB1_SOLUTION_ADF_Prediction notebook, computer a Fermentation ID column from `Fermentation_Start_Time`
all_brands_df = all_brands_df.astype({"Fermentation Start Time": "datetime64"})


def fermentation_id(fv_id, t0):
    tt = str(t0).split(" ")
    return f"{fv_id}_{tt[0].replace('-','')}"


all_brands_df["Fermentation ID"] = all_brands_df.apply(
    lambda row: fermentation_id(row["Asset_Id"], row["Fermentation Start Time"]), axis=1
)<jupyter_output><empty_output><jupyter_text>2d. Main Analysis  Analysis parametersSet brand of beer for the analysis<jupyter_code># selected brand
BRAND = "Realtime Hops"<jupyter_output><empty_output><jupyter_text>Plot data from data view result for brand of interest, keep only important brewing phase<jupyter_code>tcbdf = arrange_by_position(
    brand_df_cleanup(all_brands_df[all_brands_df["Brand"] == BRAND])
)
fig = px.scatter(
    tcbdf,
    x="Timestamp",
    y="Temperature",
    color="Position",
    hover_name="Ferm_ID",
    title=f"Temperature for {BRAND} brand and phases in {IMPORTANT_BREWING_PHASES}",
)
fig.update_layout(
    xaxis=dict(
        rangeselector=dict(
            buttons=list(
                [
                    dict(count=7, label="1w", step="day", stepmode="backward"),
                    dict(count=14, label="2w", step="day", stepmode="backward"),
                    dict(count=1, label="1m", step="month", stepmode="backward"),
                    dict(step="all"),
                ]
            )
        ),
        rangeslider=dict(visible=True),
        type="date",
    ),
)
fig.update_traces(hovertemplate=None)
fig.layout.title.font=dict(size=14)
fig.layout.template.layout.hovermode = "x unified"
fig.show()<jupyter_output><empty_output><jupyter_text>Compute cooling elapsed timeAs seen in Part 1, `prepare_cooling_data` calls `brand_df_cleanup` and `cooling_data_extraction` to compute a new cooling elapsed time column.<jupyter_code>cool_df = prepare_cooling_data(all_brands_df, BRAND)
cool_df<jupyter_output><empty_output><jupyter_text>Rearrange dataframe and plot batches data<jupyter_code>pos_cool_df = arrange_by_position(cool_df)
fig = px.scatter(
    pos_cool_df,
    x=COOLING_TIME,
    y="Temperature",
    color="Position",
    facet_col="Ferm_ID",
    title=f"Cooling Temperature Profile per Batch for {BRAND} Brand",
)
for i in fig["layout"]["annotations"]:
    i["font"] = dict(size=11, color="#000000")
    i["text"] = i["text"].replace("=", "=<br>")
fig.layout.template.layout.hovermode = "x unified"
fig.show()<jupyter_output><empty_output><jupyter_text>Regression parametersThe computation of the non-linear regression curve will pick the set data defined by two criteria: * A set of temperature zones with truth value to identify if the zone should contribute (`TEMP_SENSORS`)* A set of index of batches to consider as regression data (`reg_batches`) All data for the regression are from curves matching the two criteria above.<jupyter_code>TEMP_SENSORS = {"Bottom": True, "Middle": True, "Top": True}  # pick all zones

# first and second batches, namely FV32_20170118 and FV32_20170331
reg_batches = [0, 1]

# list of all possible batch ID
batch_ids = list(pos_cool_df["Ferm_ID"].unique())

# list of sensor zones (position) which are selected (True)
sensors_to_use = [repr(pos) for pos in Position if TEMP_SENSORS[repr(pos)]]

# from a list of indices, return associate list of batch ID
def batch_select(ids):
    return [batch_ids[i] for i, _ in enumerate(batch_ids) if i in ids]


print(f"all batch IDs={batch_ids}\nselected batch(es)={batch_select(reg_batches)}")
fig = px.scatter(
    pos_cool_df[
        (
            pos_cool_df["Ferm_ID"].isin(batch_select(reg_batches))
            & (pos_cool_df["Position"].isin(sensors_to_use))
        )
    ],
    x=COOLING_TIME,
    y="Temperature",
    color="Position",
    facet_col="Ferm_ID",
    title=f"Cooling Temperature Profiles for {BRAND} as Regression Input Data",
)
for i in fig["layout"]["annotations"]:
    i["font"] = dict(size=11, color="#000000")
    i["text"] = i["text"].replace("=", "=<br>")
fig.layout.template.layout.hovermode = "x unified"
fig.show()
alpha, beta, max_cooling_days = compute_cooling_parameters(
    pos_cool_df, BRAND, TEMP_SENSORS, batch_select(reg_batches)
)
alpha, beta, max_cooling_days
# build dataframe with all batches and their associated prediction
all_df = pd.DataFrame()
for batch_id in batch_ids:
    _, _, cfdf = generate_prediction_df(
        alpha,
        beta,
        BRAND,
        pos_cool_df,
        TEMP_SENSORS,
        batch_id,
        max_cooling_days,
        len(batch_select(reg_batches)),
    )
    all_df = all_df.append(cfdf)<jupyter_output><empty_output><jupyter_text>2e. Plot prediction curve along with actual data **Note:** You can zoom into the graph to see how the prediction and data actually differ.<jupyter_code>fig = px.scatter(
    all_df,
    x=COOLING_TIME,
    y="Temperature",
    color="Position",
    facet_col="Ferm_ID",
    title=f"Cooling Temp with Regression from {batch_select(reg_batches)}"
    f" and {[repr(pos) for pos in Position if TEMP_SENSORS[repr(pos)]]}<br>"
    f"for brand {BRAND}"
)
fig.layout.template.layout.hovermode = "x unified"
fig.layout.title.font = dict(size=14)
for i in fig["layout"]["annotations"]:
    i["font"] = dict(size=11, color="#000000")
    i["text"] = i["text"].replace("=", "=<br>")
fig.show()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10578947368421053, 'fraction_numerical': 0.009962406015037594, 'mean_word_length': 4.422136159804321, 'pattern_counts': {'":': 6, '<': 81, '<?xml version=': 0, '>': 83, 'https://': 8, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '19', 'dir': 'jupyter-structured-clean-dedup', 'id': '629451', 'license': 'Apache-2.0', 'n_tokens_mistral': 8300, 'n_tokens_neox': 7812, 'n_words': 2673, 'path': 'NB2_EXERCISE_Cooling_Prediction.ipynb', 'repo_name': 'academic-hub/brewery-exercises', 'size': '26600'}