基于Python的传感器数据采集与分析
1. 数据采集基础
在进行传感器数据采集时,我们可以通过特定的代码逻辑来触发数据的收集和分享。以下是一段示例代码:
if( push > 10 ) {
for( int i= 0; i != 16; ++i ) {
gather_data();
share_data();
}
}
heartbeat();
这段代码的逻辑是,当
push
的值大于10时,会循环16次执行
gather_data()
和
share_data()
函数,之后执行
heartbeat()
函数。这允许我们将卡片放置在离传感器已知的距离处,按下按钮,传感器会通过串行接口发送一小段数据。然后我们可以移动卡片并收集另一组数据。
2. 使用Python进行数据建模和分析
我们将使用
pyserial
模块在Python中编写一个独立的数据收集应用程序。为了使程序正常工作,我们需要关闭Arduino IDE,以便Python程序可以访问USB串行端口。
串行接口会接收到一系列单独的位,这些位可以重新组合成字节。信号的底层序列会以定义的速率在高电压和低电压之间切换,这个速率称为波特率。除了波特率,还有许多其他参数定义了串行接口的配置。
在某些情况下,我们可以将接口配置总结为
9600/8 - N - 1
。这表示我们将以9600波特的速率交换位,使用8位字节,不进行奇偶校验,并在数据位之后包含一个停止位。
/
后面的
8 - N - 1
规范是广泛使用的默认值,可以安全地假定。但9600波特的传输速度不能默认,需要明确指定。我们在Arduino的
setup()
函数中使用
Serial.begin(9600)
指定了9600波特,这意味着我们的Python数据收集应用程序也必须指定9600波特。Arduino的默认配置是
SERIAL_8N1
,
pyserial
的默认配置也是如此。
即使Arduino默认不使用流控制,在我们的Python软件中最好始终使用超时选项。如果Arduino停止发送(或接收),我们的Python应用程序可以继续运行。这意味着在超时间隔结束时,如果没有可用的输入,我们会观察到一个空的输入行。我们可以忽略这些空行并正常处理其他内容。
3. 从串行端口收集数据
-
不同操作系统下的串行端口表示
:
-
在Mac OS X上,当物理设备插入USB接口时,会创建串行端口设备的操作系统表示。我们会在文件系统中看到物理设备显示为类似
/dev/cu.*的名称。可以在连接Arduino前后使用ls dev/cu.*命令来查看。 -
在Windows上,行为可能有所不同,因为即使没有连接设备,
COM0:和COM1:端口也可能存在。
-
在Mac OS X上,当物理设备插入USB接口时,会创建串行端口设备的操作系统表示。我们会在文件系统中看到物理设备显示为类似
- Arduino IDE对数据采集的影响 :当Arduino IDE运行时,它会打开串行接口以便上传草图。由于Mac OS X或Linux的串行设备只能由单个进程独占使用,因此在Arduino IDE运行时,我们无法从Python应用程序捕获数据。上传草图后,我们就不再需要Arduino IDE,可以安全地退出。
-
Python与Arduino的通信
:为了与Arduino通信,我们可以使用
serial.Serial创建一个可用于输入或输出的文件。以下是一个基本的连接示例代码:
import serial
import sys
def sample(port, baud=9600, limit=128):
with serial.Serial(port, baud, timeout=1) as ir_sensor:
while limit != 0:
line = ir_sensor.readline()
if line:
print( line.decode("ascii").rstrip() )
sys.stdout.flush()
limit -= 1
这个
sample()
函数将从Arduino收集最多128行输入。它打开指定的串行接口设备,通常是
"/dev/cu.usbmodem1421"
。我们提供了更改波特率设置的功能,由于9600是常见的默认值,我们将其作为
baud
参数的默认值。
超时值为1意味着接口将仅等待1秒以获取完整的输入行。如果没有完整的行可用,串行接口的
readline()
方法将返回一个零长度的字节字符串
b''
。如果有完整的行(包括末尾的
b'\r\n'
)可用,将返回这个字节字符串。
超时对于防止接口在等待Arduino提供输入时卡住至关重要。为了在结果可用时立即显示,我们使用了
sys.stdout.flush()
。这将确保输出一旦可用就显示在Python控制台中。这虽然不是必需的,但它是一个方便的调试功能。我们经常希望立即得到一切正常工作的反馈,因为可能存在Python错误、Arduino草图错误或布线问题。在Arduino草图中使用
heartbeat()
函数和在Python中使用
sys.stdout.flush()
为我们提供了一种评估问题根源的方法。
4. 格式化收集的数据
原始的串行输出中包含
'\t'
字符,在Python中可以直接使用。我们可以使用
csv
模块来解析这些数据。可以使用
csv.reader(ir_sensor, delimiter='\t')
打开一个读取器,以正确分割列。
为了进行长期分析,将数据保存到使用更常见的分隔符
","
的CSV文件中是很有用的。同时,包含一个标题行也很有帮助,这样我们就知道文件中各种整数的含义。
为了进行校准,我们希望将预期距离和实际的Arduino模拟输入值作为简单的对序列一起使用。这将使我们能够创建统计模型,将预期距离读数与输入值相关联。
以下是Python端的数据收集代码,分为三个函数:
-
过滤Arduino数据的函数
:
def gather(ir_sensor, samples=16):
while samples != 0:
line = ir_sensor.readline()
if line:
yield line.decode("ascii").rstrip()
samples -= 1
这个生成器函数将从Arduino读取样本数据行。它要求串行接口已正确打开并作为
ir_sensor
参数提供。样本数量的上限默认值为16。这个生成器是一种过滤器,它会拒绝空行,同时也是一种将字节转换为正确Unicode字符的映射。
-
重新格式化一次实验数据的函数
:
import csv
def experiment_run(ir_sensor, writer, expected, samples=16):
rdr = csv.reader(gather(ir_sensor, samples), delimiter='\t')
for row in rdr:
data = dict(
Start= row[0],
Stop= row[1],
Raw= row[2],
# Ignore 12 columns of measurement and count pairs.
Expected= expected,
)
writer.writerow(data)
这个函数接受串行接口作为
ir_sensor
参数,以及一个CSV
DictWriter
对象作为输出。我们还提供了预期测量值,以便添加到每行数据中,并指定要收集的样本数量。
-
主数据收集函数
:
def collect_data():
with serial.Serial(port, baud, timeout=1) as ir_sensor:
with open("ir_data.csv", "w", newline="") as results:
wtr = csv.DictWriter(results, ["Start", "Stop", "Raw", "Expected"])
wtr.writeheader()
exp_str = input('Expected [q]: ')
while not exp_str.lower().startswith('q'):
try:
exp = int(exp_str)
distance_run(ir_sensor, wtr, exp)
except ValueError as e:
print(e)
exp_str = input('Expected [q]: ')
这个函数将打开Arduino的串行接口以开始收集数据,同时打开一个数据收集文件以保存最终数据。我们使用了
DictWriter
,以便在收集的数据文件上有一致的标题。它会提示用户输入预期距离,如果用户输入
'q'
或
'quit'
,将结束
while
语句并关闭两个文件。如果用户输入有效的预期距离数字,将使用
distance_run()
函数在给定的预期距离处收集16个样本。
输出将是一个简单的CSV文件,如下所示:
Start,Stop,Raw,Expected
39248264,39253316,118,30
39255860,39260908,118,30
这显示了数据收集的开始时间和结束时间,单位为微秒,我们期望这两个时间至少相差5000微秒。原始值也来自Arduino,预期值由用户输入,部分定义了实验设置。
5. 数据统计分析
- 校准过程 :我们的校准过程如下。首先,将代码下载到Arduino。然后退出Arduino IDE并启动我们的Python数据收集应用程序。我们向Python程序输入一个距离,然后将一张纸卡放在离传感器已知的距离处。当我们按下原型上的数据收集按钮时,按钮的LED会在收集数据时闪烁。我们可以输入一个新的距离,移动卡片,按下按钮并收集更多数据。
-
数据统计计算
:
-
我们可以使用
statistics模块来查看每个样本集合的平均值、标准差和方差。我们需要根据每次收集16个样本时记录的预期距离值对样本进行分组。以下是相关代码:
-
我们可以使用
from collections import defaultdict
from statistics import mean, stdev
def distance():
by_dist = defaultdict(list)
for filename in 'irdata_0.csv', 'irdata_1.csv', 'irdata_2.csv':
with open(filename) as source:
for row in nsreader(source):
by_dist[row.Expected].append(row)
for d in sorted(by_dist):
m = mean( row.Raw for row in by_dist[d] )
s = stdev( row.Raw for row in by_dist[d] )
count = len(by_dist[d])
print( "{d}cm n={count}: µ={m:7.3f}, "
"σ={s:8.4f}".format_map(vars()) )
print()
我们创建了一个
collections.defaultdict
,它将包含所有具有相同预期距离的项目列表。然后我们可以比较三次校准运行中预期距离为15cm的测量值。
以下是一个示例输出:
irdata_0.csv
15cm n=16: µ=421.812, σ= 3.5444
20cm n=16: µ=254.438, σ= 2.9432
25cm n=16: µ=214.125, σ= 0.6191
我们只看每次运行中15cm的数据:
15cm n=16: µ=421.812, σ= 3.5444
15cm n=16: µ=299.438, σ= 2.3085
15cm n=16: µ=300.312, σ= 1.0145
显然,第一次运行测量的结果与第二次和第三次运行不同。该设置中肯定有某些不同之处。数据的变化更大,平均值与其他两次运行有显著差异。超过90%的数据应该落在给定平均值的三个标准差范围内。平均值约为300,标准差约为2.3,我们预计数据落在293到307之间。平均值约为421的情况极不可能属于同一总体。
6. 创建线性模型
大多数红外设备的数据表显示电压和距离之间存在一种反幂曲线关系。我们只测量了15cm到30cm之间的性能,这只是设备整体能力范围的一小部分。限制范围的一个原因是该范围的这一部分看起来是线性的。另一个原因是12英寸的书桌尺子只覆盖约30cm。米尺或码尺会显示不同的结果。
我们可以通过一个小的变换将非线性幂曲线数据转换为合适的幂曲线。例如,我们可以使用
1/raw
来转换原始值,从而在更广泛的距离范围内实现更准确的位置计算。这种转换留给需要在更大距离上获得更高精度的人。我们的数据仅在15cm到30cm的短范围内测量,并且在该范围内看起来是线性的。
为了创建读数和距离之间的转换,我们将创建一个线性模型。这将为我们提供方程
y = βx + α
的两个参数。在这种情况下,
y
是线性距离,单位为厘米,
x
是输入读数(理想情况下是电压,但我们会发现原始读数在短距离内也适用)。
β
是直线的斜率,
α
是
x
等于零时的
y
轴截距。
Python的
statistics
模块不包含线性估计函数。在互联网上搜索会发现许多方法。我们将依赖以下计算来得到两个系数:
[
\beta = r\times\frac{\sigma_y}{\sigma_x}
]
[
\alpha = \mu_y - \beta\times\mu_x
]
其中,(\sigma_x)和(\sigma_y)是
x
和
y
值的标准差,(\mu_x)和(\mu_y)是这两个变量的平均值,
r
是皮尔逊相关系数。我们可以按以下方式计算它:
[
r = \frac{\sum_{i}(z(x_i)\times z(y_i))}{N - 1}
]
其中,
N
是样本数量,(z(x_i))和(z(y_i))是根据值、总体平均值和总体标准差计算的标准化z分数。我们将每个原始值转换为与平均值的标准差数量:
[
z(x_i)=\frac{x_i - \mu_x}{\sigma_x}
]
[
z(y_i)=\frac{y_i - \mu_y}{\sigma_y}
]
以下是实现线性估计的Python函数:
from statistics import mean, stdev, pstdev
def z( x, μ_x, σ_x ):
return (x - μ_x)/σ_x
def linest( pairs ):
x_seq = tuple(p[0] for p in pairs)
y_seq = tuple(p[1] for p in pairs)
μ_x, σ_x = mean(x_seq), stdev(x_seq)
μ_y, σ_y = mean(y_seq), stdev(y_seq)
z_x = (z(x, μ_x, σ_x) for x in x_seq)
z_y = (z(y, μ_y, σ_y) for y in y_seq)
r_xy = sum( zx*zy for zx, zy in zip(z_x, z_y) )/len(pairs)
beta = r_xy * σ_y/σ_x
alpha = μ_y - beta*μ_x
return r_xy, alpha, beta
我们确保输入是一个
(x, y)
对的列表。我们将计算一个模型,从
x
值预测
y
值。我们将对序列分解为
x
值序列和
y
值序列。我们计算两个序列的平均值和标准差,并使用两个生成器表达式应用
z()
函数,将原始值序列转换为标准化值序列。得到标准化的z值后,我们可以计算两个序列之间的相关性,由
r_xy
变量表示。从
r_xy
值,我们计算将
x
值映射到
y
值的线性模型的
alpha
和
beta
参数。
我们可以使用以下函数来显示线性模型:
def correlation(filename, xform=lambda x:x):
with open(filename) as source:
data = nsreader(source)
pairs = list( (xform(row.Raw), row.Expected) for row in data)
r, alpha, beta = linest(pairs)
r_2 = r*r
print( "r² = {r_2:.4f}".format_map(vars()) )
print( "d = {beta:.5f}*raw + {alpha:.2f}".format_map(vars()) )
这个函数将从我们收集的数据文件中提取
Raw
和
Expected
属性值。我们使用
nsreader()
创建命名空间对象,这允许我们使用
row.Expected
这样的语法来引用样本的属性。我们对每个
Raw
值应用
xform
函数。默认的
xform
函数是一个不做任何操作的lambda对象。我们可以提供不同的函数(或lambda对象)来探索可能需要的任何变换,以使原始数据线性化。
通过以上步骤,我们可以完成传感器数据的采集、分析和建模,为后续的应用和研究提供有力的支持。
基于Python的传感器数据采集与分析
7. 线性模型的应用与评估
线性模型在传感器数据的处理中有着重要的应用。通过
correlation
函数得到的线性模型可以帮助我们根据传感器的原始读数预测距离。例如,当我们得到了线性模型的参数
beta
和
alpha
后,就可以通过公式
d = beta * raw + alpha
来计算距离
d
。
在评估线性模型的质量时,我们使用了
r²
值(决定系数)。
r²
值越接近1,表示模型对数据的拟合程度越好。以下是一个示例,展示了如何使用
correlation
函数并解释其输出:
correlation('irdata_0.csv')
假设输出如下:
r² = 0.9500
d = 0.01234*raw + 10.20
从这个输出中,我们可以知道:
-
r² = 0.9500
:说明模型对数据的拟合程度很高,超过95%的数据可以由该线性模型解释。
-
d = 0.01234*raw + 10.20
:这是具体的线性模型公式,我们可以根据传感器的原始读数
raw
来计算距离
d
。
8. 数据处理流程总结
为了更清晰地理解整个数据处理流程,我们可以用一个流程图来表示:
graph TD;
A[下载代码到Arduino] --> B[退出Arduino IDE];
B --> C[启动Python数据收集应用程序];
C --> D[输入预期距离];
D --> E[放置卡片并按下收集按钮];
E --> F[收集16个样本数据];
F --> G[保存数据到CSV文件];
G --> H[进行数据统计分析];
H --> I[创建线性模型];
I --> J[评估线性模型];
这个流程图展示了从开始到最终得到线性模型并评估的整个过程。具体步骤如下:
1. 下载代码到Arduino,为数据采集做准备。
2. 退出Arduino IDE,以便Python程序可以访问串行端口。
3. 启动Python数据收集应用程序。
4. 输入预期距离,这是实验的一个重要参数。
5. 放置卡片并按下收集按钮,触发数据采集。
6. 收集16个样本数据,确保数据的完整性。
7. 将收集到的数据保存到CSV文件,方便后续分析。
8. 对CSV文件中的数据进行统计分析,如计算平均值、标准差等。
9. 根据统计分析结果创建线性模型。
10. 评估线性模型的质量,判断其是否适合用于实际应用。
9. 常见问题及解决方法
在整个数据采集和分析过程中,可能会遇到一些常见问题,以下是一些问题及对应的解决方法:
| 问题 | 原因 | 解决方法 |
| ---- | ---- | ---- |
| Python程序无法连接到串行端口 | Arduino IDE正在运行,占用了串行端口 | 退出Arduino IDE,确保串行端口可以被Python程序访问 |
| 收集到的数据包含大量空行 | 串行接口超时,导致出现空行 | 使用
gather
函数过滤空行,确保数据的有效性 |
| 线性模型的
r²
值较低 | 数据可能存在噪声或非线性关系 | 检查数据质量,尝试对原始数据进行变换,如使用
xform
函数 |
| 不同文件的统计结果差异较大 | 实验设置可能不同或存在测量误差 | 检查实验设置,确保每次实验的条件一致,同时增加样本数量以减少误差 |
10. 拓展与优化建议
为了进一步提高数据采集和分析的效果,我们可以考虑以下拓展和优化建议:
-
拓展数据范围
:目前我们只测量了15cm到30cm之间的数据,可以尝试扩大测量范围,如从10cm到50cm,以获得更全面的传感器性能数据。
-
使用更复杂的模型
:对于非线性关系更明显的数据,可以考虑使用多项式回归、神经网络等更复杂的模型来提高预测的准确性。
-
增加数据预处理步骤
:在数据收集后,可以进行滤波、平滑等预处理操作,去除噪声,提高数据质量。
-
自动化实验流程
:可以编写脚本实现自动化的实验流程,减少人工操作的误差和时间成本。例如,自动输入预期距离、自动收集数据等。
通过以上的拓展和优化,可以使我们的传感器数据采集和分析系统更加完善,为实际应用提供更可靠的支持。
11. 总结
本文详细介绍了基于Python的传感器数据采集与分析的全过程。从数据采集的基础代码开始,我们学习了如何使用
pyserial
模块与Arduino进行通信,收集传感器数据。接着,我们对收集到的数据进行了格式化处理,将其保存为CSV文件。然后,通过统计分析和线性模型的创建,我们可以对传感器的性能进行评估,并建立起读数与距离之间的关系。
在整个过程中,我们使用了多个Python模块,如
serial
、
csv
、
statistics
等,这些模块为我们的数据处理提供了强大的工具。同时,我们还介绍了常见问题的解决方法和拓展优化建议,帮助读者更好地应对实际应用中的挑战。
通过学习本文的内容,读者可以掌握传感器数据采集与分析的基本方法,为进一步的研究和应用打下坚实的基础。无论是在科研领域还是工业应用中,这些知识都具有重要的价值。
超级会员免费看
1401

被折叠的 条评论
为什么被折叠?



