Forecasting
A Simple Approach for Addressing Behind-the-Meter Solar Generation
I was recently asked to account for behind-the-meter (BTM) solar generation in a set of day-ahead hourly electric models. Fundamentally, the problem is that electricity consumption (from the perspective of the utility or the ISO) is lower on sunny days, when rooftop solar panels are generating lots of power, thereby reducing the electricity that customers pull from the grid. In many cases, we have little, if any, data on the capacity or generation of these rooftop panels. Further, we may not even have the necessary weather variables.
In this case, I did not have rooftop solar-generation capacity or solar irradiance (i.e., a measure of the solar energy striking a panel). I was not going to let a lack of data stop me!
I did have light intensity, measured in foot-candles. Although not identical to solar irradiance, light intensity is similar enough for our purposes.
The following figure shows the hourly light intensity for a one week period. The pattern is clear: light intensity increases in the morning hours; it typically reaches a peak during the middle of the day; and it declines in the afternoon. And, the height of the curve varies from day-to-day, partly as a function of cloud-cover.
The following figure shows a three-year period of light intensity at noon. My intuition suggests that the spikey values along the top are erroneous and should be excluded from the model. For instance, I doubt that the maximum value is roughly 5,000 in the summer, but occasionally jumps to 7,000.
To address these spikes, I built transforms to impose a maximum allowable value for each interval. The resulting series for noon appears below. It’s not perfect, but it does look more realistic. I used visual inspection for each interval to determine the appropriate maximum value.
Although I did not have data on solar capacity, my assumption for this area is that BTM capacity is increasing over time. To account for the increasing BTM capacity, I created an interaction variable, which multiplies the light-intensity variables by a linear time-trend variable. The result is a series that moves up and down from day-to-day, as the intensity of the light changes, but which also increases over time, as shown below.
When I drop this variable onto the right-hand side of a regression equation, the variable is highly significant with a negative sign, which is the correct theoretical relationship: more sunshine leads to lower loads.
The following is the BX tab from a MetrixND regression model. The figure shows the contribution of the trended solar variable (in red) and the model’s predicted value (in blue). As expected, the contribution to the total predicted value is negative and becomes larger in absolute terms (i.e., more negative), thus reducing the load by larger quantities over time.
While imperfect and admittedly simple, this approach lowers the in-sample MAPE during the middle of the day by roughly 0.3 percent Clearly, the improvement in the statistical models is not dramatic. Still, this technique allows us to incorporate, in a very basic way, an issue that is becoming an increasing concern.
In this case, I did not have rooftop solar-generation capacity or solar irradiance (i.e., a measure of the solar energy striking a panel). I was not going to let a lack of data stop me!
I did have light intensity, measured in foot-candles. Although not identical to solar irradiance, light intensity is similar enough for our purposes.
The following figure shows the hourly light intensity for a one week period. The pattern is clear: light intensity increases in the morning hours; it typically reaches a peak during the middle of the day; and it declines in the afternoon. And, the height of the curve varies from day-to-day, partly as a function of cloud-cover.
The following figure shows a three-year period of light intensity at noon. My intuition suggests that the spikey values along the top are erroneous and should be excluded from the model. For instance, I doubt that the maximum value is roughly 5,000 in the summer, but occasionally jumps to 7,000.
To address these spikes, I built transforms to impose a maximum allowable value for each interval. The resulting series for noon appears below. It’s not perfect, but it does look more realistic. I used visual inspection for each interval to determine the appropriate maximum value.
Although I did not have data on solar capacity, my assumption for this area is that BTM capacity is increasing over time. To account for the increasing BTM capacity, I created an interaction variable, which multiplies the light-intensity variables by a linear time-trend variable. The result is a series that moves up and down from day-to-day, as the intensity of the light changes, but which also increases over time, as shown below.
When I drop this variable onto the right-hand side of a regression equation, the variable is highly significant with a negative sign, which is the correct theoretical relationship: more sunshine leads to lower loads.
The following is the BX tab from a MetrixND regression model. The figure shows the contribution of the trended solar variable (in red) and the model’s predicted value (in blue). As expected, the contribution to the total predicted value is negative and becomes larger in absolute terms (i.e., more negative), thus reducing the load by larger quantities over time.
While imperfect and admittedly simple, this approach lowers the in-sample MAPE during the middle of the day by roughly 0.3 percent Clearly, the improvement in the statistical models is not dramatic. Still, this technique allows us to incorporate, in a very basic way, an issue that is becoming an increasing concern.
Kesalahan terjadi ketika Memproses Template.
The following has evaluated to null or missing:
==> authorContent.contentFields [in template "44616#44647#114455" at line 9, column 17]
----
Tip: It's the step after the last dot that caused this error, not those before it.
----
Tip: If the failing expression is known to legally refer to something that's sometimes null or missing, either specify a default value like myOptionalVar!myDefault, or use <#if myOptionalVar??>when-present<#else>when-missing</#if>. (These only cover the last step of the expression; to cover the whole expression, use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)??
----
----
FTL stack trace ("~" means nesting-related):
- Failed at: contentFields = authorContent.content... [in template "44616#44647#114455" at line 9, column 1]
----
1<#assign
2 webContentData = jsonFactoryUtil.createJSONObject(author.getData())
3 classPK = webContentData.classPK
4/>
5
6<#assign
7authorContent = restClient.get("/headless-delivery/v1.0/structured-contents/" + classPK + "?fields=contentFields%2CfriendlyUrlPath%2CtaxonomyCategoryBriefs")
8contentFields = authorContent.contentFields
9categories=authorContent.taxonomyCategoryBriefs
10authorContentData = jsonFactoryUtil.createJSONObject(authorContent)
11friendlyURL = authorContentData.friendlyUrlPath
12authorCategoryId = "0"
13/>
14
15<#list contentFields as contentField >
16 <#assign
17 contentFieldData = jsonFactoryUtil.createJSONObject(contentField)
18 name = contentField.name
19 />
20 <#if name == 'authorImage'>
21 <#if (contentField.contentFieldValue.image)??>
22 <#assign authorImageURL = contentField.contentFieldValue.image.contentUrl />
23 </#if>
24 </#if>
25 <#if name == 'authorName'>
26 <#assign authorName = contentField.contentFieldValue.data />
27 <#list categories as category >
28 <#if authorName == category.taxonomyCategoryName>
29 <#assign authorCategoryId = category.taxonomyCategoryId />
30 </#if>
31 </#list>
32 </#if>
33 <#if name == 'authorDescription'>
34 <#assign authorDescription = contentField.contentFieldValue.data />
35
36 </#if>
37
38 <#if name == 'authorJobTitle'>
39 <#assign authorJobTitle = contentField.contentFieldValue.data />
40
41 </#if>
42
43</#list>
44
45<div class="blog-author-info">
46 <#if authorImageURL??>
47 <img class="blog-author-img" id="author-image" src="${authorImageURL}" alt="" />
48 </#if>
49 <#if authorName??>
50 <#if authorName != "">
51 <p class="blog-author-name">By <a id="author-detail-page" href="/w/${friendlyURL}?filter_category_552298=${authorCategoryId}"><span id="author-full-name">${authorName}</span></a></p>
52 <hr />
53 </#if>
54 </#if>
55 <#if authorJobTitle??>
56 <#if authorJobTitle != "">
57 <p class="blog-author-title" id="author-job-title" >${authorJobTitle}</p>
58 <hr />
59 </#if>
60 </#if>
61 <#if authorDescription??>
62 <#if authorDescription != "" && authorDescription != "null" >
63 <p class="blog-author-desc" id="author-job-desc">${authorDescription}</p>
64 <hr />
65 </#if>
66 </#if>
67</div>
The following has evaluated to null or missing: ==> authorContent.contentFields [in template "44616#44647#114455" at line 9, column 17] ---- Tip: It's the step after the last dot that caused this error, not those before it. ---- Tip: If the failing expression is known to legally refer to something that's sometimes null or missing, either specify a default value like myOptionalVar!myDefault, or use <#if myOptionalVar??>when-present<#else>when-missing</#if>. (These only cover the last step of the expression; to cover the whole expression, use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)?? ---- ---- FTL stack trace ("~" means nesting-related): - Failed at: contentFields = authorContent.content... [in template "44616#44647#114455" at line 9, column 1] ----
1<#assign
2 webContentData = jsonFactoryUtil.createJSONObject(author.getData())
3 classPK = webContentData.classPK
4/>
5
6<#assign
7authorContent = restClient.get("/headless-delivery/v1.0/structured-contents/" + classPK + "?fields=contentFields%2CfriendlyUrlPath%2CtaxonomyCategoryBriefs")
8contentFields = authorContent.contentFields
9categories=authorContent.taxonomyCategoryBriefs
10authorContentData = jsonFactoryUtil.createJSONObject(authorContent)
11friendlyURL = authorContentData.friendlyUrlPath
12authorCategoryId = "0"
13/>
14
15<#list contentFields as contentField >
16 <#assign
17 contentFieldData = jsonFactoryUtil.createJSONObject(contentField)
18 name = contentField.name
19 />
20 <#if name == 'authorImage'>
21 <#if (contentField.contentFieldValue.image)??>
22 <#assign authorImageURL = contentField.contentFieldValue.image.contentUrl />
23 </#if>
24 </#if>
25 <#if name == 'authorName'>
26 <#assign authorName = contentField.contentFieldValue.data />
27 <#list categories as category >
28 <#if authorName == category.taxonomyCategoryName>
29 <#assign authorCategoryId = category.taxonomyCategoryId />
30 </#if>
31 </#list>
32 </#if>
33 <#if name == 'authorDescription'>
34 <#assign authorDescription = contentField.contentFieldValue.data />
35
36 </#if>
37
38 <#if name == 'authorJobTitle'>
39 <#assign authorJobTitle = contentField.contentFieldValue.data />
40
41 </#if>
42
43</#list>
44
45<div class="blog-author-info">
46 <#if authorImageURL??>
47 <img class="blog-author-img" id="author-image" src="${authorImageURL}" alt="" />
48 </#if>
49 <#if authorName??>
50 <#if authorName != "">
51 <p class="blog-author-name">By <a id="author-detail-page" href="/w/${friendlyURL}?filter_category_552298=${authorCategoryId}"><span id="author-full-name">${authorName}</span></a></p>
52 <hr />
53 </#if>
54 </#if>
55 <#if authorJobTitle??>
56 <#if authorJobTitle != "">
57 <p class="blog-author-title" id="author-job-title" >${authorJobTitle}</p>
58 <hr />
59 </#if>
60 </#if>
61 <#if authorDescription??>
62 <#if authorDescription != "" && authorDescription != "null" >
63 <p class="blog-author-desc" id="author-job-desc">${authorDescription}</p>
64 <hr />
65 </#if>
66 </#if>
67</div>