# No ambiente Jupyter - fazer gráfico no próprio ambiente usando a MATPLOTLIB
%matplotlib inline
# Importação dos princiapais módulos utilizados em análises de dados de processo
# Dica:
# Sempre procurar tratar as exceções para facilitar a manutenção dos códigos e a utilização por terceiros!
try:
import numpy as np
import datetime
import pytz
import matplotlib.pyplot as plt
from scipy import signal
import statsmodels as sm # https://www.statsmodels.org/stable/install.html (ver instalador via gitHub)
from statsmodels.tsa import seasonal
from statsmodels.tsa.stattools import adfuller # https://www.statsmodels.org/stable/examples/index.html#stats
import pandas as pd
from gekko import GEKKO
import getpass
import itertools
# Importação do módulo para conecão com o EPM Server (via EPM Web Server)
import epmwebapi as epm
print('Módulos importados com sucesso!')
except ImportError as error:
print('Erro na importação!')
print(error.__class__.__name__ + ': ' + error.message)
except Exception as exception:
print(exception.__class__.__name__ + ': ' + exception.message)
C:\Anaconda3\lib\site-packages\statsmodels\tools\_testing.py:19: FutureWarning: pandas.util.testing is deprecated. Use the functions in the public API at pandas.testing instead. import pandas.util.testing as tm
Módulos importados com sucesso!
# ATENÇÃO:
# Para uso em produção, é recomendado usar variáveis de ambiente para buscar informações de Usuário/Senha.
# Para este minicurso será utilizado as funções input e getpass
user = input('EPM user:')
password = getpass.getpass("EPM password:")
epm_auth = 'http://epmtr.elipse.com.br:44333' # 'http://localhost:44333'
epm_web = 'http://epmtr.elipse.com.br:44332' # 'http://localhost:44332'
# Criação de uma conexão informando os endereços do EPM Webserver(Authentication Port e WEB API Port), usuário e senha.
try:
epmConn = epm.EpmConnection(epm_auth, epm_web, user, password)
# A forma mais recomendada (fácil) para usar o comando print é com fstring
print(f'Conexão com {epm_web} criada com sucesso para o usuário {user}.')
except:
print(f'Falha no estabelecimento da conexão com {epm_web} para o usuário {user}.')
EPM user:mauricio EPM password:········ Conexão com http://epmtr.elipse.com.br:44332 criada com sucesso para o usuário mauricio.
# Buscando um perído com uma variável bem ruidosa :) - já interpoalada a cada segundo
bv_LIC101 = epmConn.getDataObjects(['LIC101'])['LIC101']
# Valores "devem" ser informados em conforemidade com o Timezone ou em UTC (Coordinated Universal Time)
ini_date = datetime.datetime(2014, 3, 4, 2, tzinfo=pytz.utc)
end_date = datetime.datetime(2014, 3, 4, 4, tzinfo=pytz.utc)
query_period = epm.QueryPeriod(ini_date, end_date)
process_interval = datetime.timedelta(seconds=1)
# Dica: usar TAB após "epm.AggregateType." para ver métodos disponíveis (intelisense)
aggregate_details = epm.AggregateDetails(process_interval, epm.AggregateType.Interpolative)
try:
data = bv_LIC101.historyReadAggregate(aggregate_details, query_period)
plt.plot(data['Value'], color='#00e6e6') # plot apenas dos dados
plt.xlabel("auto-linear")
plt.ylabel(bv_LIC101.name)
fig = plt.gcf()
ax = plt.gca()
fig.set_size_inches(18, 10)
fig.patch.set_facecolor('#525454')
ax.set_facecolor('#323434')
except Exception as exception:
print(exception.__class__.__name__ + ': ' + exception.message)
# Aplicando 3 filtros com frequências diferentes para ver o efeito da definição correta da mesma
cut_off_list = [0.7, 0.3, 0.1] # N vezes a frequência de Nyquist
color_list = ['#f7c539', '#23702e', '#9d479e']
order = 2
yf = []
for wn in cut_off_list:
b, a = signal.butter(order, wn)
yf.append(signal.filtfilt(b, a, data['Value']))
plt.plot(data['Value'], color='#00e6e6') # plot apenas dos dados
c = 0
for y in yf:
plt.plot(y, color=color_list[c]) # plot dos valores filtrados
c += 1
plt.xlabel("auto-linear")
plt.ylabel(bv_LIC101.name)
fig = plt.gcf()
ax = plt.gca()
fig.set_size_inches(18, 10)
fig.patch.set_facecolor('#525454')
ax.set_facecolor('#323434')
legend = ['não filtrado'] + list(map(str, cut_off_list)) # Transforma lista de num. em de Strings
ax.legend(legend)
<matplotlib.legend.Legend at 0x1e72d41d6a0>
# Uma abordagem simples é aplicar um delta simples (faz um shift unitário dos dados e subtrai)
delta = data['Value'][1:] - data['Value'][:-1]
t = np.arange(len(delta))
plt.plot(t, data['Value'][1:], color='#00e6e6') # plot dos dados originais
plt.plot(t, delta, color='#9d479e') # plot do delta
plt.xlabel("auto-linear")
plt.ylabel(bv_LIC101.name)
fig = plt.gcf()
ax = plt.gca()
fig.set_size_inches(18, 10)
fig.patch.set_facecolor('#525454')
ax.set_facecolor('#323434')
ax.legend(['original', 'delta'])
<matplotlib.legend.Legend at 0x1e72d53a630>
# Dados sem aplicação do delta
r1 = adfuller(data['Value'][1:])
print('ADF Statistic: %f' % r1[0])
print('p-value: %f' % r1[1])
print('Critical Values:')
for key, value in r1[4].items():
print('\t%s: %.3f' % (key, value))
# Dados com a aplicação do delta
r2 = adfuller(delta)
print('ADF Statistic: %f' % r2[0])
print('p-value: %f' % r2[1])
print('Critical Values:')
for key, value in r2[4].items():
print('\t%s: %.3f' % (key, value))
# ADF_1 -4 é menor que -3 (a 5 %) , isso sugere que podemos rejeitar H0, ou seja, sinal é estacionário!
# ADF_2 -15 é MUITO menor que -3.4 (a 1 %) , isso sugere que podemos rejeitar H0, ou seja, sinal é estacionário!
ADF Statistic: -3.833713 p-value: 0.002581 Critical Values: 1%: -3.431 5%: -2.862 10%: -2.567 ADF Statistic: -15.388304 p-value: 0.000000 Critical Values: 1%: -3.431 5%: -2.862 10%: -2.567
# Suponde a adição de uma constante no nosso sinal original
n = len(data['Value'])
t = np.arange(n)
y_with_trend = data['Value'] + 3 * np.linspace(0, 5, n) # Adicionando uma tendência linear positiva
fig, ax = plt.subplots(2, 1)
ax[0].plot(t, data['Value'])
ax[0].set_xlabel('Time points')
ax[0].set_ylabel('LIC_101')
ax[1].plot(t, y_with_trend)
ax[1].set_xlabel('Time points')
ax[1].set_ylabel('LIC_101 + trend')
fig.set_size_inches(18, 10)
fig.patch.set_facecolor('#525454')
ax[0].set_facecolor('#323434')
ax[1].set_facecolor('#323434')
# Removendo a tendência adicionada
y_dtr = signal.detrend(y_with_trend, type='linear')
fig, ax = plt.subplots(2, 1)
ax[0].plot(t, y_with_trend)
ax[0].set_xlabel('Time points')
ax[0].set_ylabel('LIC_101 + trend')
ax[1].plot(t, y_dtr)
ax[1].set_xlabel('Time points')
ax[1].set_ylabel('LIC_101 + DE-trended')
fig.set_size_inches(18, 10)
fig.patch.set_facecolor('#525454')
ax[0].set_facecolor('#323434')
ax[1].set_facecolor('#323434')
$\tau \frac{\mathrm{d} y(t)}{\mathrm{d} t} + y(t) = k u(t)$
$k$: ganho do processo $(\frac{\Delta y}{\Delta u})$
$\tau$ : constante de tempo do processo (tempo correspondente a apoximadamente 63% $\Delta y$)
# BUSCANDO OS DADOS da variável de entrada (OP ou u, como preferir :) )
ini_ss_pos = 50
y_meas = yf[2][ini_ss_pos:]
n = len(y_meas)
t = np.linspace(0,7200,n) # create time vector
# u
bv_FIC101 = epmConn.getDataObjects(['FIC101'])['FIC101']
try:
u = bv_FIC101.historyReadAggregate(aggregate_details, query_period)
u_meas = u['Value'][ini_ss_pos:]
fig, ax = plt.subplots(2, 1)
ax[0].plot(t, y_meas, 'y')
ax[0].set_xlabel('Time points')
ax[0].set_ylabel('LIC_101')
ax[1].step(t, u_meas)
ax[1].set_xlabel('Time points')
ax[1].set_ylabel('FIC_101')
fig.set_size_inches(18, 10)
fig.patch.set_facecolor('#525454')
ax[0].set_facecolor('#323434')
ax[1].set_facecolor('#323434')
except Exception as exception:
print(exception.__class__.__name__ + ': ' + exception.message)
# Usnado o GEKKO para simular um modelo de primeira ordem com tempo morto
# Alternativamente poderia utilizar o módulo SciPy!
m = GEKKO()
m.time = t
# Modelo para simulação dinânica
m.options.IMODE = 4 # simulação dinâmica
# 1 - Steady-state simulation (SS)
# 2 - Model parameter update (MPU)
# 3 - Real-time optimization (RTO)
# 4 - Dynamic simulation (SIM)
# 5 - Moving horizon estiomation (EST)
# 6 - Nonlinear control / dynamic optimization (CTL)
# 7 - Sequential dynamic simulation (SQS)
# 8 - Sequential dynamic estimation (SQE)
# 9 - Sequential dynamic optimization (SQO)
#
k = 2.0 # visualmente estima Delta_y / Delta_u
tau = 10.0 # viasualmente estima em 10s
pv = m.Var(value=y_meas[0])
op = m.Param(value=u_meas)
m.Equation(tau*pv.dt()+pv==k*op) # Equação diferencial de primeira ordem
m.solve(disp=False)
fig, ax = plt.subplots(2, 1)
ax[0].plot(t, y_meas, 'y--')
ax[0].plot(m.time, pv.value, 'r--')
ax[0].set_xlabel('Time points')
ax[0].set_ylabel('LIC_101')
ax[1].step(m.time, op.value)
ax[1].set_xlabel('Time points')
ax[1].set_ylabel('FIC_101')
fig.set_size_inches(18, 10)
fig.patch.set_facecolor('#525454')
ax[0].set_facecolor('#323434')
ax[1].set_facecolor('#323434')
ax[0].legend(['original', 'visual'])
<matplotlib.legend.Legend at 0x1e72df4b9e8>
# Utilizando o GEKKO para estimar os parâmetros k e tau
# Alternativamente poderia utilizar o ScyPy-Optimization-Minimization!
mo = GEKKO()
mo.options.IMODE = 2 # Estimativa de parâmetros
k_est = mo.FV(value=1.0, lb=1.0, ub=10.0) # Ganho do processo - FV: Fixed Values por um horizonte de temp o
k_est.STATUS = 1 # Ativa o status da variável a ser otimizada (indica que é para otimizar)
tau_est = mo.FV(value=5.0, lb=0.1, ub=100.0) # Constante de tempo
tau_est.STATUS = 1 # Ativa o status da variável a ser otimizada
y_m = mo.Param(value=y_meas)
u_m = mo.Param(value=u_meas)
y_p = mo.Var(value=y_meas[0])
mo.Equation(tau_est*y_p.dt()+y_p==k_est*u_m) # Equação diferencial de primeira ordem
#mo.Obj(((y_p-y_m)/y_m)**2) # Função objetivo - normalizando deu um ganho menor, porém mais rápido
mo.Obj(((y_p-y_m))**2) # Função objetivo
mo.solve(disp=False)
print(f'k estimated: {k_est.value[0]}')
print(f'tau estimated: {tau_est.value[0]}')
k estimated: 2.1219091946 tau estimated: 49.992472299
# Simulando com os novos parâmetros estimados
m2 = GEKKO()
m2.time = t
# Modelo para simulação dinânica
m2.options.IMODE = 4 # simulação dinâmica
k2 = k_est.value[0]
tau2 = tau_est.value[0]
pv2 = m2.Var(value=y_meas[0])
op2 = m2.Param(value=u_meas)
m2.Equation(tau2*pv2.dt()+pv2==k2*op2) # Equação diferencial de primeira ordem
m2.solve(disp=False)
fig, ax = plt.subplots(2, 1)
ax[0].plot(m2.time, y_meas, 'y--')
ax[0].plot(m2.time, pv2.value, 'g--')
ax[0].plot(m.time, pv.value, 'r--')
ax[0].set_xlabel('Time points')
ax[0].set_ylabel('LIC_101')
ax[1].step(m2.time, op2.value)
ax[1].set_xlabel('Time points')
ax[1].set_ylabel('FIC_101')
fig.set_size_inches(18, 10)
fig.patch.set_facecolor('#525454')
ax[0].set_facecolor('#323434')
ax[1].set_facecolor('#323434')
ax[0].legend(['original', 'estimado', 'visual'])
<matplotlib.legend.Legend at 0x1e732fbfc18>
# BUSCANDO OS DADOS de Agosto, Setembro e Outubro/2016 (3 meses) de um Aerogerador (pot. 2KW)
wind_speed = epmConn.getDataObjects(['Aero01_Wind_speed'])['Aero01_Wind_speed']
wind_direction = epmConn.getDataObjects(['Aero01_Wind_direction'])['Aero01_Wind_direction']
active_power = epmConn.getDataObjects(['Aero01_Active_power'])['Aero01_Active_power']
ini_date = datetime.datetime(2016, 8, 1, 3, tzinfo=pytz.utc) # local-time: 2016-08-01 00:00:00
end_date = datetime.datetime(2016, 11, 1, 3, tzinfo=pytz.utc) # local-time: 2016-11-01 00:00:00
query_period = epm.QueryPeriod(ini_date, end_date)
process_interval = datetime.timedelta(minutes=10)
# Dica: usar TAB após "epm.AggregateType." para ver métodos disponíveis (intelisense)
aggregate_details = epm.AggregateDetails(process_interval, epm.AggregateType.TimeAverage)
try:
bv_list = [wind_speed, wind_direction, active_power]
data = [bv.historyReadAggregate(aggregate_details, query_period) for bv in bv_list]
timestamp = data[0]["Timestamp"]
print(f'Primeiro timestamp está em UTC: {timestamp[0]}')
except Exception as exception:
print(exception.__class__.__name__ + ': ' + exception.message)
Primeiro timestamp está em UTC: 2016-08-01 03:00:00+00:00
# Convertendo para a data-hora Local (America/Sao_Paulo)
# tz_sp = pytz.timezone('America/Sao_Paulo') - prece haver um problema com o timezone 'America/Sao_Paulo' (6 min)
tz = datetime.timezone(datetime.timedelta(hours=-3)) # Desta forma se utiliza explicitamente o Off-set!
def utc2tz(dt, tz):
""" Converte date-time em UTC para timezone informada - removendo tzinfo
"""
return dt.astimezone(tz).replace(tzinfo=None)
local_timestamp = list(map(utc2tz, timestamp, [tz]*len(timestamp)))
print(f'Primeiro timestamp em hora local: {local_timestamp[0]}')
Primeiro timestamp em hora local: 2016-08-01 00:00:00
# Definição de uma função que organiza os dados de Séries Temporais do EPM em um Dataframe do Pandas
def epm2pandas(timestamp_index=None, **kwargs):
# É necessário que todos tenham o mesmo timestamp para usar como index
# ex: df = epm2pandas(var1=epmDataVar1, var2=epmDataVar2, var3=epmDataVar3) # pode ter qtas variáveis quiser! :)
df = None
def newByteOrderAndVectorize(epm):
# Reordena os bytes para o pandas funcionar corretamente numpay array do EPM
v = epm['Value'].byteswap().newbyteorder()
return v.reshape(len(v),1)
if len(kwargs)>0:
columnNames = []
notFirst = False
for key, value in kwargs.items():
columnNames.append(key)
if notFirst:
v = newByteOrderAndVectorize(value)
data = np.hstack((data, v))
else: # primeira coluna de timestamp é utilizada para todos as demais variáveis
notFirst = True
data = newByteOrderAndVectorize(value)
if timestamp_index is None:
timestamp_index = value['Timestamp']
df = pd.DataFrame(data, index=timestamp_index, columns = columnNames)
return df
print('Função que converte dados do EPM em um Dataframe do Pandas definida com sucesso!')
Função que converte dados do EPM em um Dataframe do Pandas definida com sucesso!
# Montando um Dataframe com os dados lidos nas 3 consulta de média ponderado ao longo do tempo a cada 10 min
df = epm2pandas(timestamp_index=local_timestamp, speed=data[0], direction=data[1], power=data[2])
df.info() # informações do Dataframe
<class 'pandas.core.frame.DataFrame'> DatetimeIndex: 13248 entries, 2016-08-01 00:00:00 to 2016-10-31 23:50:00 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 speed 13248 non-null float32 1 direction 13248 non-null float32 2 power 13248 non-null float32 dtypes: float32(3) memory usage: 258.8 KB
# Obtendo informações (visão geral) sobre os dados
df.describe() # Estatísticas dos dados
fig, axes = plt.subplots(nrows=3, ncols=1, figsize=(18, 10))
df.plot(subplots=True, ax=axes) # Plota tudo, com suporte a Sub-plots! :)
fig.patch.set_facecolor('#525454')
for ax in axes:
ax.set_facecolor('#323434')
# Aplicando um filtro para pegar apenas os dados do mês de setembro
data_sep = df[(df.index > datetime.datetime(2016, 9, 1)) & (df.index < datetime.datetime(2016, 10, 1))]
print('HEAD')
print(data_sep.head())
print(10 * '-')
print('TAIL')
print(data_sep.tail())
HEAD speed direction power 2016-09-01 00:10:00 2.515 285.244995 -0.065 2016-09-01 00:20:00 2.655 284.895020 -0.070 2016-09-01 00:30:00 2.615 284.165009 -0.070 2016-09-01 00:40:00 2.550 282.835022 -0.065 2016-09-01 00:50:00 2.630 281.705017 -0.070 ---------- TAIL speed direction power 2016-09-30 23:10:00 8.285 190.514999 898.084961 2016-09-30 23:20:00 7.425 188.160004 681.674988 2016-09-30 23:30:00 7.120 183.980011 547.835022 2016-09-30 23:40:00 7.815 182.184998 692.940002 2016-09-30 23:50:00 8.040 185.699997 770.119995
ax = plt.subplot(111, polar=True)
ax.set_theta_zero_location('N')
ax.set_theta_direction(-1)
ax.grid(True)
ax.xaxis.set_ticklabels(['N',r"$45^{o}$",'E',r"$135^{o}$",'S',r"$225^{o}$",'W', r"$315^{o}$"])
ax.set_title("Wind direction", va='bottom')
ax.plot(data_sep.direction, data_sep.speed, 'bo')
fig = plt.gcf()
fig.set_size_inches(12, 12)
fig.patch.set_facecolor('#525454')
ax.set_facecolor('#323434')
# Pegando apenas os dias úteis do mês de setembro (Seguda à Sexta-feira)
data_busines_day = data_sep[data_sep.index.dayofweek < 5] # Seg=0, Ter=1...Sab=5, Dom=6,
print(f'Quantidade de dados do mês se setembro: {len(data_sep)}')
print(f'Quantidade de dados dos DIAS ÚTEIS do mês se setembro: {len(data_busines_day)}')
Quantidade de dados do mês se setembro: 4319 Quantidade de dados dos DIAS ÚTEIS do mês se setembro: 3167
colors = data_busines_day.power/1000.0
area = np.pi * data_busines_day.speed**2
ax = plt.subplot(111, polar=True)
ax.set_theta_zero_location('N')
ax.set_theta_direction(-1)
ax.grid(True)
ax.xaxis.set_ticklabels(['N',r"$45^{o}$",'E',r"$135^{o}$",'S',r"$225^{o}$",'W', r"$315^{o}$"])
c = plt.scatter(data_busines_day.direction, \
data_busines_day.speed,\
c=colors, s=area, cmap=plt.cm.hsv)
c.set_alpha(0.75)
ax.set_title("Wind direction - speed/power - only September Business Days", va='bottom')
#ax.plot(data_sep.direction, data_sep.speed, 'bo')
fig = plt.gcf()
fig.set_size_inches(12, 12)
fig.patch.set_facecolor('#525454')
ax.set_facecolor('#323434')
# Salvando em um arquivo .CSV
data_busines_day.to_csv(r'c:\temp\Sep_data_busines_day.csv', header=True)
print('Confere se está lá mesmo!!! :)')
Confere se está lá mesmo!!! :)
# PERIODIGRAM - PSD (Power Spectral Density)
# Auxilia na captura da frequência do processo estocástico e identifica periodicidades
# Para o caso particular de análise do espectro para vento, ver artigo:
# How to calculate wind spectra: https://www.nwpsaf.eu/publications/tech_reports/nwpsaf-kn-tr-008.pdf
# Analizando os dados dos 3 meses da velocidade do vento
data_speed = df.speed.to_numpy()
fs = 10.0 * 60; # frequencia de amostragem em segundos
n = len(data_speed)
t = np.arange(n) / fs
# An improved method, especially with respect to noise immunity, is Welch’s method!
f, Pper_spec = signal.welch(data_speed, fs, scaling='spectrum')
fig, ax = plt.subplots()
ax.semilogy(f, Pper_spec) # plot do espectro
ax.set_xlabel('frequency (Hz)')
ax.set_ylabel('PSD')
fig.set_size_inches(18, 10)
fig.patch.set_facecolor('#525454')
ax.set_facecolor('#323434')
$Y_t = f(T_t, C_t, S_t, \varepsilon_t)$
onde:
Referências:
decomp = seasonal.seasonal_decompose(data_speed, freq=24, model='additive') # ou 'multiplicative'
# ref.: https://www.statsmodels.org/stable/generated/statsmodels.tsa.seasonal.seasonal_decompose.html
# freq: Frequency of the series. Must be used if x is not a pandas object
fig = decomp.plot()
#axs = plt.get_axes()
axs = fig.get_axes()
fig.set_size_inches(18, 10)
fig.patch.set_facecolor('#525454')
for ax in axs:
ax.set_facecolor('#323434')
mod = sm.tsa.arima_model.ARIMA(data_speed, order=(2, 2, 1))
results = mod.fit()
fig = results.plot_predict()
fig.set_size_inches(18, 10)
fig.patch.set_facecolor('#525454')
for ax in fig.get_axes():
ax.set_facecolor('#323434')
# SEMPRE deve-se encerrar a conexão estabelecida com o EPM Server, pois isso irá encerrar a sessão e
# liberar a licença de EPM Client para que outros, eventualmente, possam utilizá-la.
epmConn.close()